块级作用域绑定
js 中通过 var 申明变量是很容易让人迷惑的,为此,ES6 引入了块级作用域来强化对变量生命周期的控制。
var 申明变量、变量提升机制
在函数作用域或全局作用域通过 var 申明的变量,无论在什么位置申明的变量,都会被当成在当前作用域顶部申明的变量,这就是变量提升机制。
function demo(a){
if(a){
var value = 'test1'; //此处申明 value 变量
}
return value; //项目编码中不要这么写,此处仅为讲解知识点
}
上面的代码在预编译阶段,js 引擎会将由 var 申明的变量提升到函数顶部,只是初始化保留在原申明的位置执行。预编译后代码会改成如下的样子:
function demo(a){
//变量申明会提升到函数顶部
var value;
if(a){
//初始化会保留在原处
value = 'test1';
}
//如果 a=false,则 value 不会被初始化,则 value=undefined
return value;
};
块级申明
let 申明变量
let 用法与 var 相同,但 let 申明的变量不会被提升,作用域限制在当前代码块中。
function demo(a){
if(a){
let value = 'test1'; //此处创建并初始化 value 变量
//当离开 if 块时,value 会立即被销毁
}else{
//当 a=false 时,value 将永远不会被申明
}
//变量 value 在此处不存在,连 undefined 都不是;
//如果写了 return value 会直接报错。
}
同一作用域内,用 let 重复申明已存在的标识符,会报错。
function demo(){
var a = 1;
var a = 2; //不会报错
let a = 3; //运行时会报错
var b = 1;
if(b){
let b =2; //不会报错,因为两处 b 的作用域不同
}
}
const 申明常量
ES6 标准提供了 const 关键字来申明常量, const 申明的常量,在申明时必须进行初始化,且一旦绑定后不可被更改。与 let 类似,const 申明的也是块级标识符,不会被提升,一旦执行到块外会立即被销毁。const 也不能重复申明已存在的标识符。
const 申明对象
ES6 中的常量与其它语言很像,但是,ES6 中的常量如果是对象,则对象中的值可以修改。因为 const 申明的常量不允许修改绑定,但允许修改值。这就意味着可以修改对象常量的属性值,只要不修改对象的内存地址。
const a; //报错,申明时必须初始化
a = 1; //报错,不能修改绑定
const b = 1; //正确
const user = { name : 'demo' };
user.name = 'test'; //可以修改常量的值
user = { name : 'ceshi' }; //报错,不能修改常量的绑定
临时死区(Temporal Dead Zone)
ECMAScript 标准中并没有提到临时死区,但我们常用其描述不提升的效果。
js 引擎在扫描代码发现变量申明时,要么将其提升到作用域顶部(var),要么将申明放到 TDZ 中(let、const)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量申明语句后,变量才会从 TDZ 中移除。但在 let/const 申明的作用域外对该变量进行 typeof 运算时,则不会报错,因为 TDZ 是相对于块级作用域而言的。
console.log(typeof a); //出错,访问了临时死区中的变量
let a = 'demo';
//不会报错,因为 b 不在当前块的作用域中,故不在 TDZ 中。
console.log(typeof b); //typeof b=undefined。 b is not defined。注意:undefined != not defined
if(condition){
let b = 'test';
}
循环中的块作用域绑定
ES6 之前的循环变量在循环体外仍能访问,但用 let 申明的变量则不可以。
for (var i = 0; i < 10; i++) {
//todo something
}
console.log(i); //10,此处依然可以访问到变量i
for (let i = 0; i < 10; i++) {
//todo something
}
console.log(i); //报错,循环结束后 i 即被销毁
循环中的 let 与 const
ES6 之前,在循环中创建函数是一件麻烦的事情,我们先看如下代码。
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push(function(){
console.log(i);
});
}
funcs.forEach(function(func){
func(); //10,10,10...
});
输出10个10,这是因为每次迭代共享着变量 i,循环结束时 i=10。如果希望输出0,1,2...,可以用即时函数实现,此处不赘述。
但在 ES6 中,用 let 就不会这样,因为每次迭代都会创建一个新变量,并以之前迭代中同名变量的值将其初始化。上面的代码,将 var 换成 let,输出的便是 0,1,2...9。PS : let 申明在循环内部的这种行为是标准中专门定义的,事实上早期的 let 实现并不包含这一行为。
let 在 for 循环中的这种行为,在 for-in 中也存在。在 for-in 或 for-of 循环中使用 const 时的行为与 let 一致。
var funcs = [];
var obj = {a:1, b:2, c:3};
//每次迭代创建新的 key,输出1,2,3。但如果把 let 改成 var,输出 3,3,3
for(let key in obj){
funcs.push(function(){
console.log(key);
});
}
//不会报错,输出1,2,3。每次迭代不会修改已有绑定,而是创建一个新的绑定。
for(const key in obj){
funcs.push(function(){
console.log(key);
});
}
全局块作用域绑定
当 var 用于全局作用域时,其创建的变量作为全局对象,会覆盖 window 对象的属性。let 或 const 申明的变量会遮蔽全局的同名变量,但不会覆盖,用 window. 还可以访问到。
var RegExp = 'hello';
console.log(window.RegExp); //var 覆盖了 window 对象的属性
let RegExp = 'hi';
console.log(RegExp); //'hi',遮蔽了window.RegExp
console.log(window.RegExp === RegExp); //false,let 不会覆盖 window 属性
总结
ES6 中引入的 let 和 const 申明的块级变量没有提升机制,且禁止重复申明同名变量,访问临时死区中的变量会报错。与 var 不同,用let 和 const 申明的循环变量,每次迭代都会创建一个新的绑定。
块级变量最佳实践:默认情况下使用 const,只有在知道变量值需要更改时才用 let。