var声明的提升

先看下面这段代码:

function getValue(condition) {
    if(condition) {
        var value = "blue";
        return value;
    } else {
        // 此处可访问变量value, 其值为undefined
        return null;
    }
    // 此处可访问变量value, 其值为undefined
}

如果你不熟悉JavaScript,可能会认为只有当condition的值为true时才会创建变量value。事实上,在预编译阶段,JavaScript引擎会将上面的函数修改成下面这样:

function getValue(condition) {
    var value;
    if(condition) {
        value = "blue";
        return value;
    } else {
        return null;
    }
}

变量value的声明被提升至函数顶部,而初始化操作留在原处执行。

再看一段代码:

for(var i=0;i<10;i++){

}
console.log(i);  // 10

这段for循环结束后,循环外的i变量并非undefined。同样也是由于i变量声明提升所致。
准确的说,ES6之前,不存在语法级的块级作用域支持,开发者往往以创建一个立即执行的函数来隔离外部世界对函数内部变量的访问权。

(function(){ ... })()

块级声明

ES6提供了let和const标识符,用于声明块级作用域的变量。
块级作用域存在于:

  • 函数内部

  • 块中(字符{和}之间的区域)

let声明

function getValue(condition) {
    if(condition) {
        let value = "blue";
        return value;
    } else {
        // 变量value在此处不存在
        return null;
    }
    // 变量value在此处不存在
}

禁止重复声明

同一作用域内,不能用let去重复声明已声明过的变量:

var count = 30;
let count = 40; // 抛出语法错误

这样子是可以的:

var count = 30; // if块外的变量
if(condition) {
    let count = 40; // 声明的是if块中的新变量
}

const声明

与let声明的区别是,const声明的是常量,其值一旦被设定后不可更改。因此声明时就必须进行初始化:

const maxItems = 30;
maxItems = 40; // 语法错误,常量不可修改
const name;  // 语法错误,未初始化

const声明的对象

const声明不允许修改绑定,但允许修改值。意味着const声明对象后,可以修改该对象的属性值。有Java后端经验的同学很容易理解,这就是Java的值传递:

const person = {
    name: "Nicholas"
};

person.name = "Greg";  // 可以修改对象属性的值

// 抛出语法错误
person = { // 不允许修改引用
    name: "Greg";
};

临时性死区(Temporal Dead Zone)

ECMAScript标准并没有明确提到TDZ,但人们常用它来描述let和const的不提升效果。
JavaScript引擎在扫码代码发现变量声明时,要么将它们提升至作用域顶部(遇到var声明),要么将它们放到TDZ中(遇到let和const声明)。访问TDZ中的变量会触发运行时错误:

if(condition) {
    console.log(typeof value);  // 引用错误, 不允许访问TDZ中的变量
    let value = "blue";  // 只有执行过声明语句后,变量才从TDZ中移除
}

可见,即便是相对不易出错的typeof操作符也无法阻挡引擎抛出错误。
在let声明的作用域外对该变量使用typeof则不会报错:

console.log(typeof value);  // "undefined"

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

typeof是在声明变量value的代码块外执行的,此时value不在TDZ中。

循环中的块级作用域

在循环中创建函数

长久以来,var声明让开发者在循环中创建函数变得异常困难,因为变量到了循环之外仍能访问:

var funcs = [];

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

funcs.forEach(function(func) {
    func();  // 输出10次数字10
});

不是预期的输出0~9,而是输出10次10。因为循环中的i变量声明提升到外部了,循环内创建的函数全部保留了对相同变量i的引用。

以往,为解决这个问题,开发者们往往使用立即调用函数表达式(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();  // 输出0、然后是1、2,直到9
});

ES6提供的let和const让我们再也无需这么折腾了,直接把var换成let就搞定:

var funcs = [];

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

funcs.forEach(function(func) {
    func();  // 输出0、然后是1、2,直到9
});

循环中使用let和const的说明

标准的for循环,每次循环后会修改变量值,因此必须使用let:

for(let i=0; i<10; i++) {}

ES6的for-in和for-of循环,由于每次迭代不会修改已有的绑定,因此可以使用const代替:

for(const key in object) {}
let values = [1, 2, 3];
for(const value of values) {}

块级绑定最佳实践

对JavaScript开发者而言,直接用let代替var也符合逻辑。这种情况下,只需注意对需要写保护的变量则使用const。随着更多的开发者迁移到ES6,另一种做法一日普及,默认使用const,只有确实需要改变变量的值时使用let。因为大部分变量的值在初始化后不应再改变,而变量值预料之外的改变是很多bug的源头。这一理念获得了很多人的支持,你不妨试试。

全局块作用域

let和const与var的另外一个区别是它们在全局作用域中的行为。当var被用于全局作用域时,它会创建一个新的全局变量作为全局对象(浏览器环境中的window对象)的属性。这意味着用var很可能会无意中覆盖一个已经存在的全局变量:

var RegExp = "Hello!";
console.log(window.RegExp);  // "Hello!"

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

即使全局对象RegExp定义在window上,也不能幸免于被var声明覆盖。同样,ncz被定义为一个全局变量,并且立即成为window的属性。JavaScript过去一直这样。

使用let或const则不会:

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

如果希望在全局对象下定义变量,仍然可以用var。这种情况常见于在浏览器中跨frame或跨window访问代码。

总而言之,跨越到ES6后,大家可以把var忘了。


zhutianxiang
1.5k 声望327 粉丝