Just JavaScript - Execution Context |
Written by Ian Elliot | |||
Monday, 29 July 2019 | |||
Page 2 of 2
An ExampleUnderstanding the idea of a shared or non-shared execution context can help avoid errors such as: var myFunction = function () { this.f = new Array(5); for (var i = 0; i < 5; i++) { this.f[i] = function () { alert(i); }; } }; The intention here is to create an array of five functions which when called displays the current value of i when the function was created. Can you see what is wrong? The execution context has the variable i in it, but it is shared between the five functions. When any of the functions are called it has the value 5 which is the last value it was assigned in myFunction. As a result if you try: myFunction(); for (i = 0; i < 5; i++) { f[i](); } You will see 5 displayed five times which is not what is required or perhaps expected. There are various ways of making this work as required but in ES5 it is tricky. If you want to let each inner function capture the current value of i when the function is defined then you have little choice but to create a separate execution context for each of the inner functions. The simplest way to do this is to introduce another function which creates the original inner functions: var myFunction = function () { this.f = new Array(5); for (var i = 0; i < 5; i++) { (function context() { var j=i; this.f[i] = function () { alert(j); }; })(); } }; The overall plan of the code is much the same as before but now the function context constructs the inner functions. It declares a new variable j which is included in the execution context of the inner functions. Notice that the context function is executed immediately because of the final parentheses. This means that the Function object corresponding to context is created and executed each time through the for loop. This creates a separate execution context for each of the inner functions each with their own version of variable j. If this seems complicated – it is, and it is inefficient in that two Function objects are created each time through the loop. ES2015 has a much easier solution – block scope. Block ScopeJavaScript before ES2015 only supported function scope variables. That is, no matter where you declared a variable its scope was the entire Function it was declared in. Not only this but all variable declarations are hoisted to the top of the Function. ES2015 introduces block scoped variables with the keywords let and const. The only difference between let and const is that, as its name suggests, a variable created using const cannot be reassigned. A block is a compound statement enclosed by curly brackets and you can use let or const to declare variables that are in scope only within the block. For example: var myVar = 1; alert(myVar); { let myVar = 2; alert(myVar); } alert(myVar); This displays 1 then 2 and then 1 again. Within the block myVar in the outer scope is no longer accessible as the block scope version of myVar is. The same thing works with const but in this case you wouldn’t be able to change the value of myVar once set. Now we come to the question of hoisting. Block scope variables are hoisted, but only to the start of their scope i.e. the block that contains them. They are also not initialized to undefined but left uninitialized. This might seem like a strange thing to do, but it is designed to block variables in the outer scope being accessible within the block before the block variable is declared. For example: var myVar = 1; { alert(myVar); let myVar = 2; alert(myVar); } If it wasn’t for the declaration of myVar within the block, the outer scoped myVar would be accessible within the block. You might think that the first alert would display the current value of the outer myVar but instead it generates a runtime error that tells you myVar isn’t defined. This is because the declaration of let myVar is hoisted to the start of the block but the variable is left undefined until the code reaches the assignment. Because of hoisting, block scope variables override outer scope variables of the same name throughout the block irrespective of where they are declared. There is another very important difference between function and block scoped variables. You can declare a function scoped variable as often as you like and it has no effect: var myVar=1; var myVar=2; is fine and equivalent to: var myVar=1; myVar=2; However, you can only declare a block scope variable once: { let myVar=1; let myVar=2; } generates a runtime error: Identifier 'myVar' has already been declared The same is true for const. You might not be able to declare a block scoped variable twice, but you can declare one within a loop or in the head of a loop. In this case the declaration causes a new instance of the variable to be created. This means you can use a block scoped variable within a loop and use it to create a unique execution context each time. For example we can write the example in the previous section much more simply using let: var myFunction = function () { this.f = new Array(5); for (var i = 0; i < 5; i++) { let j = i; this.f[i] = function () { alert(j); }; } }; This works and each function now gets its own instance of the variable j. You can also write the function as: var myFunction = function () { this.f = new Array(5); for (let i = 0; i < 5; i++) { this.f[i] = function () { alert(i); }; } } Again the execution context has a new instance of i each time through the loop. This also works with const. Notice that neither example works if you change let to var. There are some other subtle behaviors caused by block scoping – for example block scoped top level variables are not global properties. It is also worth mentioning that function statements are also block scoped in ES2015 but hoisted in the usual way to the start of their scope. Summary
This is an extract from the book Just JavaScript by Ian Elliot. Buy Now: from your local AmazonJust JavaScript
|
|||
Last Updated ( Monday, 29 July 2019 ) |