前言:块绑定
在传统意义上,变量声明工作的方式在Js一直是棘手的编程部分,在大多数基于C语言的编程语言中,变量(或绑定)被创造在声明出现的地方,然而在Js中情况并不是这样的。在Js中,你的变量实际上被创造依赖于你怎样声明它们,并且在ECMAScript6中提供了更容易控制作用域的选择。这一章阐述了为什么经典的var声明是易混淆的,介绍了在ECMAScript6中的块级绑定,并且提供了一些特别好的使用它们的实例。
var 声明和提升
用var声明的变量无论真实的声明出现在函数的哪个地方都被处理好像它们处于函数的顶部(或者在全局作用域,如果它是定义在一个函数的外面),这被称为提升,对于提升的作用的实例,思考下面这个函数定义:
function getValue(condition){ if(condition){ var value = "blue"; //other code return value; } else{ //value exsits here with a value of undefined return null; } //value exsits here with a value of undefined }
如果你不熟悉Js,你可能会期望变量 value
仅仅在condition
这个条件为真的情况下被创建。而事实上,变量value
是无论如何都会被创建。在这个函数情形下,Javascript引擎改变了getValue
函数,像下面这样:
function getValue(condition) { var value; if(condition){ value = "blue"; // other code return code; } else{ return null; } }
value
的声明被提升到顶部,然而初始化仍然在相同的地方。这意味着变量value
实际上在else
子句中仍然是可访问的。如果从else
子句访问,由于未初始化,变量将仅仅有一个undefined
的值。
这常常花费新的Js开发者一些时间去适应声明提升,并且常常误解独特的行为会最终导致bugs.为此,ECMAScript6提出了块级作用域选项去更好地控制 一个变量的生命周期。
块级声明
块级声明是指在函数中声明的变量在给定块级范围外是不可访问的。块级作用域也称为词作用域,被这样创建:
1.在一个函数的内部
2.在一个块的内部(被字符 {
和 }
标识)
块级作用域是许多基于C语言的编程语言的工作方式,并且在ECMAScript6中块级声明的提出旨在为Javascript提出相同的灵活性(和一致性)。
Let 声明
let
的声明语法和 var
的语法相同。你基本上能用 let
替换 var
去声明一个变量, 但是限制变量的作用域仅仅在当前代码块(有一些其他微妙的差异也会在之后讨论)。由于 let
声明不被提升在封闭块的顶部,你可能总是想放 let
声明在封闭块的最开始位置,以便使它们是可用的在整个块内是可访问的。下面是个例子:
function getValue(condition) { if (condition) { let value = "blue"; // other code return value; } else { //value doesn't exist return null; } // value does't exist }
这个版本的 getValue
函数的运作更接近于你所期望的它在基于C的编程语言中的实现。因为变量 value
被声明用 let
而不是 var
,这个声明将不会提升到函数定义的顶部,并且一旦函数执行到 if
块的外面,变量 value
将不再是可访问的, 如果 condition
条件为 false,那么 value
将从不会声明和初始化。
无重复声明
如果一个标识符在一个作用域中早已被定义,然后在这个作用域中用 let
声明这个标识符会造成一个错误抛出。举例:
var count = 30; // Syntax error let count = 40;
在这个例子中,count
被声明两次:一次用 var
,一次用 let
。因为 let
将不会重新定义一个已经在相同作用域内存在的标识符,所以let
声明将抛出一个错误。另一方面,如果 一个 let
声明在变量的包含作用域内以同样的名字创建一个新的变量将没有错误抛出,如下面代码所述:
var count = 30; //Does not throw an error if (condition) { let count = 40; //more code }
这个 let
声明没有抛出错误是因为它是在 if
语句范围中创建了一个名为 count
的新的变量,而不是在外围代码块中创建的。在 if
代码块内部,这个新的变量覆盖了全局变量 count
,阻止访问这个全局变量直到执行流离开这个 if
语句这个代码块 。
常量声明
在ECMAScript6中你也能用 const
声明语法定义一个变量。用 const
声明的变量被当做一个常量,这意味着它们的值一旦被设定将不能改变。为此,每一个 const
变量必须在声明的时候进行初始化,如下所示:
//Valid constant const maxItems = 30; //Syntax error: missing initialization const name;
因为maxItem
变量被初始化了,所以它的 const
声明应该没有问题地工作。然而如果你尝试去运行包含这个代码的程序, name
变量将造成一个语法错误,这是因为 name
变量没有被初始化。
常量声明 vs Let声明
常量声明,像 let
声明,是块级声明。这意味着一旦执行流运行到常量被声明的代码块的外面时,常量将不再是可访问的,并且声明不被提升,如下所示:
if (condition) { const maxItem = 5; // more code } // maxItem isn't accessible here
在这段代码中,常量 maxItem
在 if
语句中被声明。一旦这个语句结束执行,在代码块的外面 maxItem
将不再是可访问的。
另一个和 let
相似的地方是:当 const
声明一个在相同的作用域内早已被定义的变量会抛出一个错误。如果这个变量用 var
声明(对于全局作用域或函数作用域)或者用 let
声明(对于块作用域内部),则它是无关紧要的。举例,思考下面代码:
var message = "hello!" let age = 25; // Each of these would throw an error. const message = ''Goodbye!'; const age = 30;
这两个 const
声明单独来说是有效的,但是鉴于在这个事件中前面的 var
和 let
声明,这两个声明都将如预期所示不工作。
尽管存在这些相同点,但是 let
和 const
之间有一个很大的不同之处需要牢记。在所有严格和非严格模式下企图给一个先前定义过的 const
常量赋值将抛出一个错误,:
const maxItem = 5; maxItem = 6; //throw error
在一个方面很像在其它语言中的常量,maxItem
变量之后不会被赋新值。然而,在另一方面不像在其他语言中的常量,如果一个常量的值是一个对象可能会被修改。
用 const 声明对象
一个 const
声明防止绑定的修改和不是它本来的的值。那意味着 const
对于对象的声明不会阻止那些对象的修改。举例:
const person = { name: "Nicholas" }; // works person.name = "Greg"; //throws an error person = { name: "Greg" }
在被绑定的 person
被创建用一个对象属性的初始化值。它是可能的去改变 person.name
没有导致错误,这是因为它只是去改变 person
所包含的属性并且并没有改变被绑定的 person
。当这段代码试图赋予一个值给 person
(因此尝试去改变这个绑定),一个错误将被抛出。这个 const
如何工作的微妙处是容易被误解的。只需要记住: const
防止绑定的修改,不是防止绑定的对象的属性值的修改。
时域死区
一个用 let
或 const
声明的变量不能被访问直到这个变量被声明后。尝试这样做将会造成引用错误,甚至当正常地使用安全操作比如在这个例子中使用 typeof
操作符也会造成错误:
if (condition) { console.log(typeof value); //ReferenceError let value = "blue"; }
这里,变量 value
用 let
声明和初始化,但是这个语句从不执行因为前一行抛出了错误。这个问题在Javascript社区被称为时间死区。时间死区在ECMAScript规范中未被明确地命名,但是这个术语被用来描述为什么 let
变量 和 const
变量不是可访问在它们未被声明前。这部分涉及了一些时间死区造成的声明位置的微妙处,并且尽管例子展示全部使用了 let
声明,注意同样的信息适用于 const
。
当Javascript引擎浏览一个即将执行的代码块并且发现一个变量声明,它要么提升这个声明到函数或全局作用域的顶部(对于 var
),要么放这个声明到时间死区(对于 let
和 const
).任何尝试在时间死区访问一个变量都会造成运行时错误。一旦执行流进行到变量声明的位置,那个变量才被移除时间死区因而可以安全使用。
这是正确的做法当你尝试去使用用 let
或 const
声明的变量在它并未被定义前。像之前的例子所阐述的,这个甚至正常地应用在安全操作符 typeof
.然而你能用 typeof
在一个变量被声明的封闭块的外面去检测这个变量的类型,尽管它可能不会给你之后声明的这个变量的结构。思考这个代码:
console.log(typeof value); // "undefined" if (condition) { let value = 'blue"; }
当 typeof
操作符执行时这个变量 value
不是在时间死区,因为它出现在变量 value
被声明的封闭块的外面。这意味着没有变量绑定,并且 typeof
操作符简单地返回 undefined
。
时间死区仅仅是块绑定的一个独特的地方。另一个不得不说的独特的地方在它们在循环中的用法。
在循环中的块绑定
也许开发者最想让变量的块级作用域存在的地方是在 for
循环中,在这种情形下,计数器变量意味着只被在循环中使用。举例来说,它是非常普遍的在JavaScript中这样的代码:
for (var i = 0; i<10; i++){ process(item[i]); } // i is still accessible here console.log(i); //10
在其他语言中,默认是块级作用域,这个例子按预期的工作,并且仅仅对于for
循环可以访问到变量 i
。然而在JavaScript中,变量 i
在循环被完成之后仍然是可访问的,因为 var
变量获得提升。相反用 let
,在下面的代码中应该得到这样的结果:
for (let i = 0; i < 10; i++ ) { process(items[i]); } // i is not accessible here --throw an error console.log(i);
在这个例子中,变量 i
仅仅在 for
循环中存在,一旦循环完成,这个变量任何其他地方不再是可访问的。
在循环中的函数
var
的特点长期以来造成了在循环内创建函数的问题,因为循环变量在循环体作用域的外面是可访问的思考下面这个例子:
var funcs = []; for (var i = 1; i < 10; i++) { funcs.push(function() { console.log(i); }); } funcs.fotEach(function(func) { func(); // output the number "10" ten times });
你可能通常希望这段代码输出数字0-9,但是它输出的是数字10十次在一行。那是因为 i
在每一次的循环迭代中是共享的,意味着在循环内创建的方法总是持有对同一变量的引用。这个变量 i
一旦循环结束则拥有值10,因而当console.log被执行时,在循环中每次值10被打印。
为了去修复这个问题,开发者在循环中用立即调用函数表达式(IIFE)去强制创建一个他们想要循环访问的新的变量副本 ,如下所示:
var funcs = []; for (var i = 0; i < 10; i++) { funcs.push(function(value) { return function() {console.log(value);} }(i)); } funcs.forEach(function(func) { func(); });
这个版本在循环的内部用一个IIFE,i
变量被传递给立即执行函数,在立即执行函数中创建它的副本并且以value
变量存储它。这是使用那个迭代函数的意义,因此每次调用函数返回了预期的循环计数从0到9的值。幸运的是,用ECMAScript6中的 let
和 const
的块级绑定对于你来说可以简化这个循环。
在循环中的Let 声明
一个 let
声明通过有效地模仿IIFE在上一个例子中所做的简化了循环。在每次迭代中,这个循环创建了一个新的变量并且初始化变量的值用上一次迭代所使用的的相同名字。那意味着你能完全省略IIFE并且获得你所期望的结果,像这样:
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
和IIFE的循环,但是可以说更简洁。let
声明通过循环每次创建了一个新的变量 i
,因此在循环体中创建的每个方法 获得了它自己i
的副本。每个 i
的副本有它在循环的迭代开始(即它被创建的那个迭代)被分配的值。对于for-in
和 for-of
是同样的道理,如这儿所示:
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" });