Chapter 4 variables, scope, and Memory

This is the 14th day of my participation in the August More Text Challenge

The book picks up where we left off with the scope problem, and then goes into more detail about variable declarations. How do variables change differently when different declarations are used? How does a variable declared by var affect a global variable? Why do LET have temporary dead zones? Is it true that a variable declared by const cannot be changed?

4.2.1 Scope chain enhancement

While there are two main execution contexts: global and function contexts (a third exists inside an eval() call), there are other ways to enhance the scope chain. Some statements cause a temporary context to be added to the front of the scope chain, which is removed after the code executes. There are two situations in which this occurs, namely when the code executes in either of the following ways:

  • The catch block of a try/catch statement
  • With statement

In both cases, a variable object is added to the front of the scope chain. For the with statement, the specified object is appended to the front of the scope chain; For a catch statement, a new variable object is created that contains the declaration of the error object to be thrown. Look at the following example:

function buildUrl() { let qs = "? debug=true"; with(location){ let url = href + qs; } return url; }Copy the code

Here, the with statement takes the Location object as the context, so location is added to the front of the scope chain. The buildUrl() function defines a variable qs. When the code in the with statement refers to the variable href, it actually refers to location.href, which is the property of its variable object. When qs is referenced, it refers to the variable defined in buildUrl(), which is defined on the variable object in the function context. The variable URL declared with var in the with statement becomes part of the function context and can be returned as the value of the function. But variable urls like the one declared here using let are not defined outside the with block because they are limited to the block-level usage domain (described later).

Note that IE implementations prior to IE8 were biased in that they would add errors caught in a catch statement to variable objects in the execution context instead of the catch statement, resulting in errors being accessible outside the catch block. IE9 corrects this problem.

4.2.2 Variable declaration

JavaScript variable declarations have undergone a sea change since ES6. Until ECMAScript 5.1, var was the only keyword used to declare variables. Not only did ES6 add the let and const keywords, it also made them overwhelmingly preferred over var.

1. Use the var function scope declaration

When a variable is declared using var, it is automatically added to the nearest context. In a function, the closest context is the local context of the function. In the with statement, the closest context is also the function context. If a variable is initialized undeclared, it is automatically added to the global context, as shown in the following example:

function add(num1, num2) { var sum = num1 + num2; return sum; } let result = add(10, 20); // 30 console.log(sum); Error: sum is not a valid variableCopy the code

Here, the function add() defines a local variable sum that holds the result of the addition operation. This value is returned as the value of the function, but the variable sum is not accessible outside the function. If you omit the var keyword in the above example, sum becomes accessible after add() is called, as follows:

function add(num1, num2) { sum = num1 + num2; return sum; } let result = add(10, 20); // 30 console.log(sum); / / 30Copy the code

This time, the variable sum is initialized with the result of the addition operation without the var declaration. After the call to add(), sum is added to the global context and remains after the function exits, making it accessible later.

Note that initializing variables without declaring them is a very common mistake in JavaScript programming that can cause a lot of problems. To do this, readers must declare variables before initializing them. In strict mode, an error is reported when a variable is initialized undeclared.

The VAR declaration is carried to the top of the function or global scope, before all the code in the scope. This phenomenon is called “ascending.” Promotion lets code in the same scope use variables without considering whether they have been declared. In practice, however, boosting can also lead to the legal but strange phenomenon of using variables before they are declared. The following example shows two pieces of equivalent code in global scope:

var name = "Jake"; // Equivalent to: var name; name = 'Jake';Copy the code

Here are two equivalent functions:

function fn1() { var name = 'Jake'; } function fn2() {var name; name = 'Jake'; }Copy the code

You can verify that the variable will be promoted by printing it before the declaration. The promotion of the declaration means that undefined will be printed instead of Reference Error:

console.log(name); // undefined 
var name = 'Jake'; 
function() { 
 console.log(name); // undefined 
 var name = 'Jake'; 
} 
Copy the code

2. Use the block-level scope declaration for lets

The new LET keyword in ES6 is similar to var, but its scope is block-level, which is also a new concept in JavaScript. The block-level scope is defined by the nearest pair of inclusion braces {}. In other words, if blocks, while blocks, function blocks, and even individual blocks are also the scope of let declaration variables.

if (true) { let a; } console.log(a); // ReferenceError: a does not define while (true) {let b; } console.log(b); // ReferenceError: b does not define function foo() {let c; } console.log(c); // ReferenceError: c is not defined // there is nothing strange about this // var declaration will also cause an error // this is not an object literal, but a separate block // JavaScript interpreter will recognize it based on its contents {let d; } console.log(d); // ReferenceError: d is not definedCopy the code

Another difference between let and VAR is that you cannot declare it twice in the same scope. Duplicate var declarations are ignored, while duplicate let declarations raise syntaxErrors.

var a; var a; {let b; let b; } // SyntaxError: identifier B has already been declaredCopy the code

The behavior of the LET is ideal for declaring iteration variables in loops. Iteration variables declared using VAR can leak out of the loop, which should be avoided. Consider the following two examples:

for (var i = 0; i < 10; ++i) {} console.log(i); // 10 for (let j = 0; j < 10; ++j) {} console.log(j); // ReferenceError: j is not definedCopy the code

Strictly speaking, let is also promoted in JavaScript runtime, but because of “temporal dead zones” you can’t actually use the let variable before declaration. Therefore, from the standpoint of writing JavaScript code, let is not promoted in the same way as VAR.

3. Use a const constant declaration

In addition to let, ES6 also adds the const keyword. Variables declared using const must also be initialized to a value. Once declared, new values cannot be reassigned at any point in its life cycle.

const a; // SyntaxError: const b = 3 was not initialized; console.log(b); // 3 b = 4; // TypeError: Assigns values to constantsCopy the code

Const is the same as a let declaration except that it follows these rules:

if (true) { const a = 0; } console.log(a); // ReferenceError: a does not define while (true) {const b = 1; } console.log(b); // ReferenceError: function foo() {const c = 2; } console.log(c); // ReferenceError: c does not define {const d = 3; } console.log(d); // ReferenceError: d is not definedCopy the code

Const declarations apply only to top-level primitives or objects. In other words, a const variable assigned to an object cannot be reassigned to another reference, but the key of the object is not restricted.

const o1 = {}; o1 = {}; // TypeError: Assign const o2 = {}; // TypeError: assign const o2 = {}; o2.name = 'Jake'; console.log(o2.name); // 'Jake'Copy the code

Freeze () to make the entire Object immutable, you can use object.freeze (), which will silently fail when assigning an attribute without an error:

const o3 = Object.freeze({}); 
o3.name = 'Jake'; 
console.log(o3.name); // undefined 
Copy the code

Because a const declaration implies that the value of a variable is of a single type and cannot be modified, the JavaScript runtime compiler can replace all instances of it with the actual value without looking up the variable through a query table. Google’s V8 engine performs this optimization.

Note that development practice suggests that if the development process is not significantly affected by this, you should use const declarations as often as possible, unless you really need a variable that will be reassigned in the future. This essentially ensures that reassignment bugs are found in advance.

4. Search for identifiers

When an identifier is referenced for reading or writing in a particular context, a search must be performed to determine what the identifier represents. The search starts at the front of the scope chain and searches for the corresponding identifier with the given name. If the identifier is found in the local context, the search stops and the variable is determined; If the variable name is not found, the search continues along the scope chain. (Note that objects in the scope chain also have a prototype chain, so a search might involve the prototype chain for each object.) This process continues until the variable object is searched to the global context. If the identifier is still not found, it is undeclared.

To better illustrate identifier lookups, let’s look at an example:

var color = 'blue'; 
function getColor() { 
 return color; 
} 
console.log(getColor()); // 'blue' 
​
Copy the code

In this example, the variable color is referenced when the function getColor() is called. A two-step search is performed to determine the value of color. The first step is to search the variable object of getColor() for the identifier named color. The result was not found, so the search continued for the next variable object (from the global context), and the identifier named color was found. Because color is defined on the global variable object, the search ends.

For this search process, referencing local variables automatically stops the search without continuing the search for the next level variable object. That is, if there is an identifier with the same name in the local context, the identifier in the parent context cannot be referenced in that context, as shown in the following example:

var color = 'blue'; 
function getColor() { 
 let color = 'red'; 
 return color; 
} 
console.log(getColor()); // 'red' 
​
Copy the code

Using block-level scope declarations does not change the search process, but can add additional layers to the lexical hierarchy:

var color = 'blue'; 
function getColor() { 
 let color = 'red'; 
 { 
 let color = 'green'; 
 return color; 
 } 
} 
console.log(getColor()); // 'green'
Copy the code

In this modified example, getColor() declares a local variable named color inside. When this function is called, the variable is declared. When executed to the function return statement, the code references the variable color. I searched for the identifier in the local context and found the variable color with a value of ‘green’. Because the variable is found, the search stops, so this local variable is used. This means that the function returns ‘green’. Any code after the declaration of the local variable color cannot access the global variable color unless the fully qualified notation window.color is used.

Note that identifier lookups are not without costs. Accessing local variables is faster than accessing global variables because you don’t have to switch scopes. However, JavaScript engines do a lot of work to optimize identifier lookups, and in the future the difference may not be significant.