0%

第一章 块级绑定

块绑定(Block Bindings)

以前在javascript中变量声明是比较复杂的一部分,大多数基于c的语言中变量(或绑定)被创建于变量声明的地方,然而javascript中并不是这样,变量创建的方式取决于你如何声明它们,并且ECMAScript 6 提供的了让控制作用域(scope)更容易的方法,在这章里将会展示为什么老式 var 声明让人困惑,同时介绍SCMAScript 6中的块绑定,并提供最佳实践。

变量声明和提升

用var声明的变量一律被当做在函数(如果在函数外部声明,则为全局变量)顶部声明的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getValue(condition) {

if (condition) {
var value = "blue";

// other code

return value;
} else {

// value exists here with a value of undefined

return null;
}

// value exists here with a value of undefined
}

如果你对javscript不熟悉,你也许会预想value这个变量只有当condition被求值为true是才被创建,但无论求condition值是什么变量value都被创建了,下面代码是javascript引擎改变getValue这个函数成这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getValue(condition) {

var value;

if (condition) {
value = "blue";

// other code

return value;
} else {

return null;
}
}

value 变量的声明被提升到顶部,同时它的初始化还在原来那个地方。这意味着value变量可以在else括号后面访问到,如果从那儿访问,这个变量将只是undefined,因为它还没被初始化。

块级声明

块级声明意味着那些声明的变量不能在给定块级作用外访问,块级作用域也称做词法作用域被创建于:

  • 1 函数里面
  • 2 双大括号里面({})
    块级作用域是许多基于c语言的工作方式,同时在ECAMScript 6 中引入块级声明意在引进相同的灵活性(并统一性)到javascript中

Let 声明

let 声明语法和var声明语法一样,你基本上可以用let替换var声明变量,但是le的变量作用域限制在当前代码块(这里有少量细微差别在后面讨论)。因为let声明不会提升到代码块顶部,你也许总想将let声明放在代码块顶部,这样在整个代码都可以获取到,这里是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getValue(condition) {

if (condition) {
let value = "blue";

// other code

return value;
} else {

// value doesn't exist here

return null;
}

// value doesn't exist here
}

这一版的getValue函数表现的更像你期待中的其他基于c的语言了,因为变量value是用let声明取代了var,这种声明并不会变量提升到函数声明顶部,并且变量value在执行完if块后不再能访问到,一旦if条件求值为false后value将永远不会声明或初始化。

禁止重声明

如果一个标识符已经在一个作用域定义过,然后再用let在这个作用域定义这个标识符将后造成一个错误抛出,如下例:

1
2
3
4
var count = 30;

//Syntax error
let count = 40;

在上面的例子中,count被声明了两次:一次用var声明然后再用一次let声明。因为let不会在一个已经声明过的作用域重声明,如果重声明将抛出一个error。另一方面,如果let声明在一个包含作用域中创建同名的变量是不会抛出错误的,如下面示例代码:

1
2
3
4
5
6
7
8
9
var count = 30;

// Does not throw an error
if (condition) {

let count = 40;

// more code
}

let声明没有抛出一个异常是因为它创建了一个新的已经声明过的变量count在if语句里,而不是在一个相邻的块。在这个if块里,这个新的变量遮盖了这个全局count,防止获取到全局count直到执行过这个if块。

常量声明

在ECMAScript6中你也可以用const声明语法定义变量。用const声明的变量被当做常量,这意味着它们的值一旦给初始化后不能改变。因为这个原因,每个count变量必须在声明的同时初始化,如下例:

1
2
3
4
5
// Valid constant
const maxItems = 30;

// Syntax error: missing initialization
const name;

maxItems 变量已经初始化了,所以cont声明将会起效没问题。如果试着运行包含name变量这段代码将会造成一个语法错我,因为name没有初始化。

const声明和let声明对比

const声明和let声明一样是块级声明。这意味着常量一旦执行完所在块后将不再能访问,并且声明也不会提升,如下面示例代码:

1
2
3
4
5
6
7
if (condition) {
const maxItems = 5;

// more code
}

// maxItems isn't accessible here

在这个代码中,常量maxItems声明在if语句里,一旦这个语句执行完成后,maxItems将在这个块外不再能访问到。

const与let另一个相似的地方,如果一个标识符在同一个作用域中已经定义过用const再次定义将会抛出一个错误。用var(全局或函数作用域)或者let(块级作用域)什么都没关系,如下例代码:

1
2
3
4
5
6
var message = "Hello!";
let age = 25;

// Each of these would throw an error.
const message = "Goodbye!";
const age = 30;

这两个const声明单独声明都会有效,但是介于前面的var和let声明没有一个conts声明将会有效。
let和const尽管这么多相似的地方,但是这里有个巨大的区别点值得记住,无论在严格或者非严格模式如果尝试赋值一个原先定义过的const变量将会抛出一个错误。

1
2
3
const maxItems = 5;

maxItems = 6; // throws error

这里的常量和其他语言很相似的是这个maxItems变量将不能重新赋值,然而不像其他语言的常量,如果这个常量是一个对象将有可能被修改。

用const声明对象

一个const声明防止其修改其绑定而非值本身。这意味着const声明一个对象将不会防止其修改这个变量的值(译者注释:属性值)。如下例:

1
2
3
4
5
6
7
8
9
10
11
const person = {
name: "Nicholas"
};

// works
person.name = "Greg";

// throws an error
person = {
name: "Greg"
};

这里,被绑定的person以一个属性值的对象创建,它是可能改变person.name而不引起一个错误的因为它是改变的person包含的属性值而并没改变person绑定到一个对象本身。当这里的代码尝试赋值person(因此尝试改变绑定本身)将会有一个错误抛出,这个在const中细微的差别容易引起误解。只要记住:const防止其改变绑定,而不是绑定的值。

临时性死区(TDZ)

一个用let或者const声明的变量不能被访问直到声明过后。尝试着这样做将会导致一个引用类型错误(reference error),即便用正常情况下安全操作符 typeof。如下例:

1
2
3
4
if (condition) {
console.log(typeof value); // ReferenceError!
let value = "blue";
}

这里,变量value用let定义和初始化,但是语句将永远不会执行因为前面句抛出一个异常。这个关于value的话题存在于javascript社区被称作临时性死区(TDZ)。TDZ在ECNAScript没有明确的定义,但是TDZ这个条目常被用来形容let或const声明时不能在其声明前访问。这个部分将讲述一些关于TDZ引起的细微差别,同时这里的例子全部用let,值得注意的是这些例子同样适用于const。

当javascript引擎遍历一个即将执行的代码块并找到一个变量声明是,它要么是var在全局或者函数内通过变量提升,抑或let或者const在TDZ的声明,任何尝试获取在TDZ的变量都将在执行时引起一个错误。只有将这个变量从TDZ移除,然后一旦执行过这个变量声明后才能安全的使用。
如上例所示let或const声明同样适用于任何尝试使用一个变量在它定义之前,尽管使用安全操作符typeof。然而当变量在声明块的外边是可以使用typeof的,尽管得到的不是你后面赋值的,看如下代码:

1
2
3
4
5
console.log(typeof value);     // "undefined"

if (condition) {
let value = "blue";
}

因为变量value没有在TDZ所有当typeof操作符执行在变量声明块外,typeof仅仅返回一个”undefined”,这意味着没有value绑定到当前作用域。
TDZ只是块绑定(block blindings)一个特别的方面,另一个方面体现在使用在循环上。

在循环中的块绑定

也许程序员最希望看到看到块级作用域出现在for循环中,循环计数变量一次性使用这意味着只能在循环中使用,但这在javascript中并不常见。

1
2
3
4
5
6
for (var i = 0; i < 10; i++) {
process(items[i]);
}

// i is still accessible here
console.log(i);

在其他语言中块级作用域是默认的,在上面这个例子中只用当i变量只能在for循环中访问到时才能到达我们预期的效果,然而在这里的i任然在for循环后可以访问到,这是因为var声明被提升了,如果用let代替,将会表现我们预期的效果,如下代码:

1
2
3
4
5
6
for (let i = 0; i < 10; i++) {
process(items[i]);
}

// i is not accessible here - throws an error
console.log(i);

在上面这个例子中,变量i只存在于for循环,一旦循环结束,变量i就不能再其他地方访问到了。

循环中的函数

var的特性为函数在循环中长期制造着问题,因为循环变量是从外层循环作用域中获取,看如下代码:

1
2
3
4
5
6
7
8
9
var funcs = [];

for (var i = 0; i < 10; i++) {
funcs.push(function() { console.log(i); });
}

funcs.forEach(function(func) {
func(); // outputs the number "10" ten times
})

你也许通常期待这份代码会打印出数字0到9,但是结果是数字10打印了十遍,那是因为在每次循环遍历中共享相同的i,一旦循环结束i的值就为10,所以当console.log(i)被调用时,i的值就被打印出来。
为了修复这个问题,开发者使用一个立即执行函数在for循环内部,强迫每次循环传递一个i的复制变量,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
var funcs = [];

for (var i = 0; i < 10; i++) {
funcs.push((function(value) {
return function() {
console.log(value);
}
}(i)));
}

funcs.forEach(function(func) {
func(); // outputs 0, then 1, then 2, up to 9
});

在这版代码中用了一个立即执行函数在循环的内部,变量i被传到立即执行函数中,这意味着创建了一份复制并储存到value中。所以每次迭代循环才能出现预期的0到9。幸运的是在EMACSript6中使用块级绑定的let和const可以轻松的做到。

循环中的let声明

一个let声明可以有效的简化和替代先前的立即执行函数(IIFE)。在每个循环遍历中,循环创建了一个新的变量并用先前遍历的变量名初始化它,这意味着你可以生路掉立即执行函数,并得到相同的结构,想下面:

1
2
3
4
5
6
7
8
9
10
11
var funcs = [];

for (let i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i);
});
}

funcs.forEach(function(func) {
func(); // outputs 0, then 1, then 2, up to 9
})

这个循环和先前的用var和立即执行函数效果一样,但是清晰,整洁。let声明在每次循环时创建一个新的变量i,所以每个函数在循环内部得到属于自己的i,同理for-in和for-of循环,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var funcs = [],
object = {
a: true,
b: true,
c: true
};

for (let key in object) {
funcs.push(function() {
console.log(key);
});
}

funcs.forEach(function(func) {
func(); // outputs "a", then "b", then "c"
});

在这个例子中for-in循环与for循环展现了同样的表现行为,每次循环遍历,一个新的key绑定创建,所以每个函数拥有属于自己的拷贝变量key,结果就是每个函数输出不同的value,如果是用var声明的key,全部的函数将输出”c”。

在循环中的常量声明

在EMASCript6中没有明确的声明不允许conts声明在循环中使用,但是这里有不同的表现行为基于你选择的for循环类型,对于普通for循环,你可以用const进行初始化,但是一旦你试着改变这个值它将抛出一个错误,如示例代码:

1
2
3
4
5
6
7
8
var funcs = [];

// throws an error after one iteration
for (const i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i);
});
}

在上面代码中,i变量以常量声明,第一次遍历循环,当i是0时,执行成功,但i++执行时将会抛出一个错误,因为它尝试改变一个常量,如此以后,你只能用const变量进行声明你不会改变的循环中。

另一方面在for-in或for-of中使用const和let声明变量并无区别,所以下面代码将不会抛出错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var funcs = [],
object = {
a: true,
b: true,
c: true
};

// doesn't cause an error
for (const key in object) {
funcs.push(function() {
console.log(key);
});
}

funcs.forEach(function(func) {
func(); // outputs "a", then "b", then "c"
});

上面这段代码几乎和上面”循环中的let声明”一模一样,唯一不同的是kety的值在循环中不能改变,const在for-of和for-in循环起作用是因为循环初始器创建了一个新的绑定在每个遍历中而不是尝试改变已经存在的绑定。

全局块绑定

let和const另一个不同于var是在全局作用域的行为,当var在全局作用域使用时,它创建了一个新的全局变量,这意味着一个全局变量属性(在浏览器中是window)。同时也意味着你也许意外的重写了一个已经存在的全局变量,如下代码:

1
2
3
4
5
6
// in a browser
var RegExp = "Hello!";
console.log(window.RegExp); // "Hello!"

var ncz = "Hi!";
console.log(window.ncz); // "Hi!"

尽管RegExp已经在全局对象window上定义过了,但是被var声明重写了这是不安全的。示例代码示范了一个新的全局变量RegExp重写原有变量。同样相似的ncz被定义成一个全局变量并且立即被定义成window上的一个属性,这是javascript的工作方式。

如果你换let或const在全局声明,在全局作用域产生一个新的绑定但是不会添加属性到全局对象上,这也意味着你不能通过let或者const重新全局变量,你只能屏蔽它,这里是实例代码:

1
2
3
4
5
6
7
8
// in a browser
let RegExp = "Hello!";
console.log(RegExp); // "Hello!"
console.log(window.RegExp === RegExp); // false

const ncz = "Hi!";
console.log(ncz); // "Hi!"
console.log("ncz" in window); // false

在这段实例代码中一个使用let声明的RegExp创建绑定并屏蔽全局RegExo。这意味着window.RegExp和RegExp并不全等,所以在全局作用域中并不存在破坏,同样,conts声明为ncz创建的绑定也不会在全局对象上创建一个属性。这种能力让let和const在全局作用域上申明更加安全。

块级绑定最佳实践进化过程

当使用使用ECMAScript6开发时,这里流传着一个信任,你应该默认使用let而不是var声明变量,对于很多javascript开发者来说,let的特性刚好是我们期待中的var。所以直接替换显得很有道理,在这种情况下,当你需要修改保护是你应该使用const声明变量。
然而当更多的开发者赚到使用ECMAScript6开发时,一个新的理念更加流行起来:默认使用const声明变量,只有当你知道变量将会改变时使用let声明。这其中的原因是大多数变量在其初始化后不要改变其值,因为预想不到的值改变通常是bugs的源头。这个想法具有巨大的吸引力并且值得你在未来使用ECMAScript6中使用。

小结

let和const的块级绑定将词法作用域引入了javscript。这些声明将不会提升只会存在于声明的块中。它们提供的表现行为将更加像其他语言并且更少的造成意想不到的错误,副作用是你将不能像以前声明一样访问它们,甚至不能用以前的安全操作符typeof。当你在块级绑定之前试图获取一个变量时将会引起一个因为TDZ造成的错误。
在许多情况下,let和const与var的表现都很相似,然而这并不适用于循环,对于for-in和for-of循环let和const都在每次循环创建一个新的绑定,而不是循环后的最后一项(像var一样)。同样的let声明也适用于for,当尝试吧const用在for循环并且改变const变量时将造成一个错误。
当前的最佳实践是默认使用const只当你知道变量将会改变时使用let,这样做会保证你代码一定程度的不可变形,从而达到防治一定类型的错误。