JavaScript Execution – I

JavaScript is an interpreted language. It means, it is executed line-by-line, and if there is an error at line 10 of the code, rest of the code (line 11 onwards) will never be executed. Even all other unrelated codes will also not execute if the JavaScript engine fails at a point and they come next in line for the execution.

To make things worse, everything happens at the run-time. In all other compiled languages, errors are identified in the compilation phase.

But, JavaScript is not entirely an interpreted language. It behaves like one though, because it does not know what’s going to happen in the next line. So the question remains, what exactly happens with JavaScript execution?

Lines of Code

The execution in JavaScript happens in two phase and the JavaScript engine divides the code in two categories.

  • Phase 1: Codes which require memory allocation and reference creation.
  • Phase 2: Codes which require access to memory location and execution.

Consider the following example:

// Example 1
1. var a = 10;
2. console.log (a);
3. var b = 20;
4. console.log (b);

Phase 1:

  • Line 1: New variable a, allocate memory for a.
  • Line 2: Skip [execution].
  • Line 3: New variable b, allocate memory for b.
  • Line 4: Skip [execution].

Once phase 1 is completed, the execution pointer [let’s say] will go back to line 1 and phase 2 will start.

Phase 2:

  • Line 1: Assignment, assign 10 to a.
  • Line 2: Execution, invoke the function console.log.
  • Line 3: Assignment, assign 20 to b.
  • Line 4: Execution, invoke the function console.log.

In case you have this question,

how does JavaScript manage to break the same line of code into two parts, one for memory allocation and the other for assignment?

Here is the answer:

Hoisting in JavaScript

Since the introduction of ES6, hoisting is not anymore in the picture, but it is still an important concept to understand the execution flow.

Hoisting in JavaScript is an interesting concept which allows us to use a variable [assign a value to it] without even declaration. However, hoisting is not limited to the undeclared variable. It happens even though we declare a variable.

In general and in ideal scenario, the correct way of creating and assigning value to a variable is:

// Example 2
var foo;            // declare a variable
foo = 10;           // assign value to the declared variable.
// Example 3
var foo;
console.log (foo);            // undefined
console.log (typeof foo);     // "undefined"
foo = 10;
console.log (foo);            // 10
console.log (typeof foo);     // "number"

It means, whenever we create a variable it becomes undefined. It is important to note that undefined is not a value.

But what happens if we try to assign the value to an undeclared variable and what happens if we try to access the value of an undeclared variable.

// Example 4
foo = 10;
console.log (foo);           // 10
console.log (typeof foo);    // "number"

// Example 5
console.log (bar);            // Uncaught ReferenceError: bar is
                              // not defined.

Let’s see how execution happens in example 4.

foo = 10;
console.log (foo);

Phase 1:

  • Line 1: Skip [assignment]
  • Line 2: Skip [execution]

Phase 2:

  • Line 1: Assignment, allocate 10 to foo
    • But it cannot assign 10 to foo, as foo doesn’t even exist.
    • So, it creates a variable foo, i.e. allocates memory for foo
    • Then assigns the value 10 to it.
  • Line 2: Execution, invoke the function console.log.

All the variable declarations are hoisted to the top of the current scope.

It means, it doesn’t matter if we declare the variable and assign the value to in different lines, JavaScript implicitly does that.

So going back to the first example, JavaScript engine treats the code as

// Example 6
1. var a;
2. var b;
3. a = 10;
4. console.log (a);
5. b = 20;
6. console.log (b);

Phase 1:

  • Line 1: New variable a, allocate memory for a.
  • Line 2: New variable b, allocate memory for b.
  • Line 3: Skip [assignment].
  • Line 4: Skip [execution].
  • Line 5: Skip [assignment].
  • Line 6: Skip [execution].

Phase 2 would be pretty much same as above, except the line numbers.

It isn’t like JavaScript rewrites all the code, breaks the assignment and declaration in two parts, and moves all the declaration to the top. But it treats the code like this. And this is what hoisting is.

Hoisting does not happen when strict mode is enabled.

The following piece of code will run fine, because of the hoisting.

// Example 7, try it out with the two phases of execution.
1. console.log (a);
2. var a = 10;
4. console.log (a);

// should output

Now, let’s revisit example 5 with an additional line of the code.

// Example 8
1. console.log (a);
2. console.log (b);
3. var a = 10;
4. b = 10;

If we execute this code, it will give us an error. And this example actually proves the point that, even though we say that all the variable declaration is moved to top, it doesn’t not happen so.

Let’s try to process this example:

Phase 1:

  • Line 1: Skip [execution].
  • Line 2: Skip [execution].
  • Line 3: New variable a, allocate memory for a.
  • Line 4: Skip [execution].

Phase 2:

  • Line 1: Execution, invoke the function console.log.
    • Should print undefined. At this point, a has no value,
  • Line 2: Execution, invoke the function console.log.
    • Should print ??. At this point, b is not known.
    • Error: Execution stops.

So, in other and clear terms, the definition of hoisting should be,

JavaScript declares a variable implicitly when it sees an assignment to an undeclared variable. This process is called variable hoisting.

Hoisting happens only for the assignment, accessing undeclared variable will result in Uncaught reference error.

Executing Functions

There are two ways to create a function,

  • Function statement/declaration/definition
  • Function expression

Read here about functions in detail.

Until now, we have not seen any example of function execution according to the steps which I have mentioned. So, before jumping into the function let’s see how execution happens for both types (based on the way they are created) of functions.

Simple function statement

// Example 9
1. function foo () {
2.   var a = "10";
3.   console.log (a);
4. }
5. foo ();
6. console.log ('Done');

Phase 1:

  • Line 1: New variable foo. Like var, all the named functions are treated as a new variable. Also, in case of functions it will not allocate memory, but it will keep a reference of the code inside the function (it would be somewhere in the memory heap).
  • Line 1-4: Ignore, i.e. jumps to next line of the end of the function.
  • Line 5: Skip [execution].
  • Line 6: Skip [execution].

Phase 2:

  • Line 1-4: Skip [declaration].
  • Line 5: Execution, invoke function foo.
  • Before going to line 6, following will take place:

But, what happens when foo is invoked, and what happened when we were invoking console.log function in all the examples above. We cannot do a walkthrough of console.log function but we can do it for foo.

When execution starts for foo, and as JavaScript knows it’s a function, it looks for the reference [the heap memory where function’s content is stored]. From this point onwards, execution of contents of function will be same as all the examples we did earlier. Now, consider the function execution as a separate program altogether, and you can execute these lines of code as we did earlier.

// contents of function foo
2.   var a = "10";
3.   console.log (a);

Break it into two phases and we are done. Once this execution is done, JavaScript execution’s Phase 2 returns to line 6. Since every JavaScript function returns a value, the Phase 2 knows where it has to go.

  • Line 6: Execution, invoke function console.log.

In the next post, we will see:

  • Execution of the function expression.
  • Execution of functions which returns a value [explicitly].
  • How hoisting works with functions.