Modern JavaScript Web Development Cookbook
上QQ阅读APP看书,第一时间看更新

Scoping variables

The concept of scope is associated with the idea of visibility: scope is the context in which defined elements (such as variables or functions) can be referenced or used. Clasically, JS provided only two types of scope: global scope (accessible everywhere) and function scope (accessible only within the function itself). Since scopes have been around since the beginning of JS, let's just remember a couple of rules, with not much elaboration:

  • Scopes are hierarchically arranged, and child scopes can access everything in the parent scope, but not the other way round.
  • Access to the parent scope will be disabled if you redefine something at an inner scope. References will always be to the child definition, and you cannot access the equally named element in the outer, encompassing scope. 

JS5 introduced a new type of scope, called block scope, that lets you work in a more careful way. This allows you to create variables for a single block, without existence outside of it, even in the rest of the function or method where they were defined. With this concept, two new ways of defining variables, other than using var, were added: let and const.

The new declarations are not subject to hoisting, so if you are not used to declaring all variables at the top of your code before they are used, you may have problems. Since the usual practice is starting functions with all declarations, this isn't likely to affect you. See  https://developer.mozilla.org/en-US/docs/Glossary/Hoisting for more details.

The first option, let, allows you to declare a variable that will be limited to the block or statement where it is used. The second option, const, adds the proviso that the variable isn't supposed to change value, but rather be constant; if you try to assign a new value to a constant, an error will be produced. The following simple examples show the new behaviors:

Using const for a constant value needs little explanation, but what about  let? The reason harkens back to the origin of the BASIC programming language. In that language, you assigned values to variables with code like 37 LET X1 = (B1*A4 - B2*A2) / D; this particular line was taken from Darmouth College's BASIC manual facsimile, dated October 1964. See  http://www.bitsavers.org/pdf/dartmouth/BASIC_Oct64.pdf for more information.
// Source file: src/let_const.js

{
let w = 0;
}
console.log(w); // error: w is not defined!

let x = 1;
{
let x = 99;
}
console.log(x); // still 1;

let y = 2;
for (let y = 999; 1 > 2; y++) {
/* nothing! */
}
console.log(y); // still 2;

const z = 3;
z = 9999; // error!

Using let also solves a classic problem. What would the following code do? Here it is: 

// Source file: src/let_const.js

// Countdown to zero?
var delay = 0;
for (var i = 10; i >= 0; i--) {
delay += 1000;
setTimeout(() => {
console.log(i + (i > 0 ? "..." : "!"));
}, delay);
}

If you were expecting a countdown to zero (10... 9... 8... down to 2... 1... 0!with suitable one second delays, you'll be surprised, because this code emits -1! eleven times! The problem has to do with closures; by the time the loop ends, the i variable is -1, so when the waiting (timeout) functions run, i has that value. This can be solved in several ways, but using let instead of var is the simplest solution; each closure will capture a different copy of the loop variable, and the countdown will be correct:

// Source file: src/let_const.js

var delay = 0;
for (let i = 10; i >= 0; i--) { // minimal fix!
delay += 1000;
setTimeout(() => {
console.log(i + (i > 0 ? "..." : "!"));
}, delay);
}