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 fora
. - Line 2: Skip [execution].
- Line 3: New variable
b
, allocate memory forb
. - 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.
- But it cannot assign 10 to foo, as
- 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 fora
. - Line 2: New variable
b
, allocate memory forb
. - 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
undefined
10
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 fora
. - Line 4: Skip [execution].
Phase 2:
- Line 1: Execution, invoke the function
console.log
.- Should print
undefined
. At this point,a
has no value,
- Should print
- Line 2: Execution, invoke the function
console.log
.- Should print ??. At this point,
b
is not known. - Error: Execution stops.
- Should print ??. At this point,
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.
Leave a Reply