第一章是关于 块级作用域绑定 的,众所周知,这是个“很沉重”的话题。
var声明及变量提升机制
通过关键字var声明的变量,无论实际上是在哪里声明的,都会被当成在当前作用域顶部声明的变量。
function getValue(condition){
if(condition){
var value="blue";
//其它代码
return value;
}else{
//此处可访问value
return null;
}
//此处亦可访问value
}
事实上,在上面的代码中,无论何种情况,变量value都会被创建。在预编译阶段,js引擎会将上面的代码修改为下面这样:
function getValue(){
var value;
if(condition){
value="blue";
//其它代码
return value;
}else{
return null;
}
}
上面的程序总有些不便之处。对“提升”的误解还会导致程序出现bug。
于是,ES6引入了 块级作用域 来强化对变量生命周期的控制。
块级声明
此声明用来声明再指定块的作用域之外无法访问的变量。它存在于:
- 函数内部
- “{}”之间
这其中,ES6最重要的莫过于 let声明 和 const声明 了。
let
let声明不会被提升。我们通常将let声明语句放在封闭代码块的顶部:
function getValue(condition){
if(condition){
let value="blue";
return value;
}else{
//此处不存在value
}
//此处不存在value了
}
禁止重声明
let不能声明已经存在过的变量——无论是用var声明过的还是用let声明过的。(但在以前ES5的var中就可以)
var count=30;
let count=40; //会报错
即,同一作用域中不能用let重复定义已经存在的标识符。但如果当前作用域内嵌另一个作用域,便可以在内嵌作用域中用let声明同名变量:
var count=30;
if(condition){
let count=40; //允许
}
除此之外,ES6还提供了const关键字,声明常量,其值不可更改——必须进行初始化!
(常量同样不会被提升至作用域顶部)
临时死区
前面说了,let和const声明的变量都不会被提升至作用域顶部。那么,如果在声明之前访问这些变量,即使是相对安全的typeof操作符也会被触发引用错误!
if(condition){
console.log(typeof value); //引用错误!
let value="blue";
}
并且,由于console.log(typrof value)语句会抛出错误,因此用let定义并初始化变量value的语句不会执行。此时说value还位于所谓的“临时死区”。
let和const的行为很多时候与var一致。然而,他们在循环中的行为却不同。 在for-in和for-of循环中,let和const都会每次迭代时创建新绑定,从而使循环体内创建的函数可以访问到对应迭代的值,而非最后一次迭代后的值。let在for循环中也是如此。
开发者可能最希望实现for循环的块级作用域了,因为可以把随意声明的计数器变量限制在循环内部
循环中的块级作用域绑定
我想起了曾经有道非常有名的面试题:
var funcs=[];
for(var i=0;i<10;i++){
funcs.push(function(){
console.log(i);
});
}
funcs.forEach(function(func){
func();
})
结果是什么?
输出0到10?
你想多了吧。。。 它会输出10个10!
为什么?
循环里每次的迭代同时共享着变量i,循环内部创建的函数全部保留了对相同变量的引用。在循环结束,i的值为10,此时才会调用console.log输出10个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();
})
这就对了嘛!
那么,我们用刚学的ES6新语法会不会更好些呢?
var funcs=[];
for(let i=0;i<10;i++){
funcs.push(function(){
console.log(i);
});
}
funcs.forEach(function(func){
func();
})
同样的结果!
let声明模仿上述事例中IIFE所作的一切来简化循环过程,每次迭代都会创建一个新变量,并以之前迭代中同名变量的值将其初始化。
全局块作用域绑定
我们知道,var在作用域全局作用域时,它会创建一个新的全局变量作为全局对象(浏览器window对象)的属性。(这意味着var很可能会无意中覆盖已经存在的全局属性)
var Reg="Hello!";
console.log(window.Reg); // "Hello!"
var R="Hi!"
console.log(window.R); // "Hi!"
他是想说,window自定义属性只能有一个!你后来创建的必定会覆盖之前创建的。
如果你使用了let或const,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。
let Reg="Hello!";
console.log(Reg); // "Hello!"
console.log(window.Reg===Reg); // false
const ni="Hi!";
console.log(ni); // "Hi!"
console.log("ni" in window); // false
如果希望在全局对象下定义变量,仍然可以使用var。这种情况常见于在浏览器中跨frame或跨window访问代码