ES6块级作用域绑定

块级作用域绑定

前言

大多数类C语言在声明变量的同时也会创建变量,而在以前的JavaScript中,何时创建变量要看怎么声明变量。因此JavaScript的变量声明机制常常让人感到困惑,ECMAScript6中的新语法可以帮助你更到的控制作用域。

var声明及变量提升(Hoisting)机制

函数作用域全局作用域中通过var关键字声明的变量,无论是在哪里声明的,都会被当成当前作用域顶部声明的变量,这就是提升(Hoisting)机制

function hoistingTest(condition) {
    //此处可以访问变量value,其值为undefined
    if (condition) {
        //此处可以访问变量value,其值为undefined
        var value = "test";
    } else {
        //此处可以访问变量value,其值为undefined
        console.log(value)
    }
    //此处可以访问变量value,其值为undefined
}

上面的示例中,你可能会认为只有当condition的值是真值时才会创建变量。事实上,无论如何变量value都会被创建。因为在预编译阶段,JavaScript引擎会将上面的函数修改成如下所示

function hoistingTest(condition) {
    var value;
    if (condition) {
        value = "test";
    } else {
        console.log(value)
    }
}

变量value的声明被提升至函数顶部,而初始操作依旧留在原处执行,这就意味着在else子句中也可以访问到变量,由于此时变量未初始化,所以其值为undefined。

块级声明

ECMAScript6引入了块级作用域来强化对变量声明周期的控制

块级声明用于声明在指定作用域之外无法访问的变量,它更符合我们的逻辑认知。块级作用域存在于:

  • 函数内部
  • 块中( {}之间的区域)

let声明

let声明的用法很简单,和var相同。用let代替var来声明变量,就可以把变量的作用域限制在当前代码块中,let的声明不会被提升,何时声明何时才会创建。如下所示:

function letTest(condition) {
    //变量value在此处不存在
    if (condition) {
        //Uncaught ReferenceError: Cannot access 'a' before initialization
        let value = "test";
        //可以访问value
    } else {
        //变量value在此处不存在
    }
    //变量value在此处不存在
}

这更符合我们平常的认知,变量value不会再被提升至函数顶部,执行流离开if块,value立刻会被销毁。如果condition值为false,就永远不会声明并初始化value。

禁止重声明

var关键字不同,当作用域已经存在某个标识符,let就不能再重复声明它

var 示例:

var a = 10;
var a = 20;
console.log(a);  //20

let示例:

var a = 10;
let a = 20;
console.log(a); //Uncaught SyntaxError: Identifier 'a' has already been declared

根据let的特性,如果当前作用域内嵌另一个作用域,便可以在内嵌的作用域中用let声明同名变量

var a = 10;
if (true) {
    //console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
    let a = 20;
    console.log(a);  //20
}
console.log(a);  //10

注意,此时不会报错,内部块中的a会遮蔽全局作用域中的a,打印的值为各个作用域中的值。

const声明

ECMAScript6还提供了const关键字。使用const声明的是常量,其值一旦被设定后不可更改。因此使用const声明的变量必须进行初始化。constlet声明的都是块级标识符,所以常量也只有在当前代码块才有效,一旦执行到代码块外会立即被销毁。常量同样也不会被提升至作用域顶部。

let相似,在同一作用域用const声明已存在的标识符也会报错,无论该标识符是使用var还是let声明的。

const count = 10;
count = 20; //Uncaught TypeError: Assignment to constant variable.

ECMAScript6中的常量与其他语言中的很像,count不可再被赋值。然后,与其他语言不同的是,JavaScript中的常量如果是对象,则对象中的值可以修改。

const person = { name: 'nick' }
person.name = "wu miao miao";
person.age = 23;
console.log(person)  // {"name":"wu miao miao","age":23}

const number = [12];
number.push(23);
number[0] = 56;
console.log(number); //[56,23]

以上这些操作都是允许的。

const声明不允许修改绑定,但允许修改绑定的值。

临时死区(Temporal Dead Zone)

上面已经说过,与var关键字不同,letconst声明的变量不会提升到作用域顶部。细心的朋友可能发现,如果在声明之前访问这些变量,不是报 Uncaught ReferenceError: b is not defined这种不存在的错误,而是Uncaught ReferenceError: Cannot access 'a' before initialization的错误

console.log(typeof b); //undefined
console.log(typeof a); //Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 10;

由于console.log(typeof a);抛出错误,因此变量a的不会被声明。此时的a位于JavaScript社区所谓的“临时死区”(temporal dead zone)TDZ中。虽然ECMAScript标准并没有明确提到TDZ,但人们常用它来描述letconst的不提升效果。

JavaScript引擎在扫描代码发现变量声明时,要么将它们提升至作用域顶部(var),要么将声明放到TDZ中(let和const)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移出,然后才能正常访问。

循环中的块作用域绑定

for (var i = 0; i < 10; i++) {
    //do something
}
console.log(i); //10

类似于这样的代码在JavaScript中很常见,在默认拥有块级作用域的其他语言中,这个示例也可以正常运行,并且变量i只能在for循环中才能访问到。而在JavaScript中,由于var 声明得到了提升,变量i在循环结束后仍可以访问。如果是用let声明就能得到想要的结果。

for (let i = 0; i < 10; i++) {
    //do something
}
console.log(i); //Uncaught ReferenceError: i is not defined

改用let声明,变量i只存在于for循环中,一旦循环结束,在其他地方均无法访问该变量。

循环中的函数

再者例如这样的代码,在循环中创建函数:

var funcs = [];
for (var i = 0; i < 10; i++) {
    funcs.push(function () {
        console.log(i);
    })
}
console.log(i); //10
funcs.forEach(function (func) {
    func();  //输出10次数字10
})

你预期的结果可能是输出数字0到9,但它却输出了10次10。这是因为循环里的每次迭代同时共享着变量i,循环内部创建的函数完全保留了对相同变量的引用。循环结束时i的值为10,所以每次调用方法主体console.log(i)时就会输出10。

为了达到预期的效果,在以前的做法是这样的:

var funcs = [];
for (var i = 0; i < 10; i++) {
    funcs.push(function (value) {
        return function () {
            console.log(value);
        }
    }(i));
}
console.log(i); //10
funcs.forEach(function (func) {
    func();  //0,1,2,3,4...9
})

我们使用立即调用函数表达式(IIFE),以强制生成计数器变量的副本value,以此来达到预期的效果。

循环中的let声明

ECMAScript6中的letconst提供的块级绑定让我们无须再这么折腾。

var funcs = [];
for (let i = 0; i < 10; i++) {
    funcs.push(function () {
        console.log(i);
    })
}
funcs.forEach(function (func) {
    func(); //0,1,2,3,4...9
})

这段循环的结果与上面的IIFE写法的调用结果完全相同。每次循环的时候let声明都会创建一个新变量i,并将其初始化为i的当前值,所以循环内部创建的每个函数都能得到属于它们自己的i的副本。对于for-infor-of来说也是一样的。

var funcs = [];
var obj = {
    a: true,
    b: true,
    c: true
}
for (let key in obj) {
    funcs.push(function () {
        console.log(key)
    })
}
funcs.forEach(function (func) {
    func();  //a,b,c
})

可以看到,let声明在for-infor中表现的行为一致。如果使用var声明key,则这些函数都会输出c。

let声明在循环内部的行为是标准中专门定义的,它不一定与let的不提升特性相关。事实上,早期的let实现不包含这一行为,它是后来加入的。

循环中的const声明

const声明与let的行为相似,但是它的特点使它在针对不同类型的循环时,会表现不同的行为。

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

在这段代码中,变量i被声明为常量。在循环的第一个迭代中,i是0,迭代执行成功。然后执行i++,因为这条语句试图修改常量i,因此抛出错误。所以,如果后续循环不会修改该变量,那可以使用const声明。

for-infor循环中使用const时的行为与let一致。

var funcs = [];
var obj = {
    a: true,
    b: true,
    c: true
}
for (const key in obj) {
    funcs.push(function () {
        console.log(key)
    })
}
funcs.forEach(function (func) {
    func();  //a,b,c
})

这段代码不会抛出错误。正如我们上面所说的,循环中没有改变key的值,每次迭代不会修改已有的绑定,而是会创建一个新绑定。

全局作用域绑定

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

console.log(window.RegExp); //function RegExp() { [native code] }
var RegExp = "Hello!"
console.log(window.RegExp); //Hello!

letconst则不会覆盖全局变量,只能遮蔽它,如下所示:

let RegExp = "Hello!"
console.log(RegExp); //Hello!
console.log(window.RegExp);  //function RegExp() { [native code] }
console.log(window.RegExp === RegExp); //false

如果你在全局作用域中使用letconst,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。

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

小结

对很多JavaScript开发者而言,let实际上与他们想要的var一样,直接替换符合逻辑。对于需要写保护的变量则要使用const。然而,当更多的开发者迁移到ECMAScript6后,另一种做法日益普及:默认使用const,只有确实需要改变变量的值时使用let。因为大部分变量的值在初始化后不应再改变,而预料外的变量值的改变是很多bug的源头。块级作用域绑定的let和const为JavaScript引入了词法作用域,它们声明的变量不会提升,而且只可以在声明这些变量的代码块中使用。如此一来,JavaScript声明变量的语法与其他语言更相似了,同时也大幅降低了产生错误的几率,因为变量只会在需要它们的地方声明。与此同时,这一新特性还存在一个副作用,即不能在声明变量前访问它们,就算用 typeof 这样安全的操作符也不行。在声明前访问块级绑定会导致错误,因为绑定还在临时死区(TDZ)中。let和const的行为很多时候与 var一致。然而,它们在循环中的行为却不一样。在 for-in 和 for-of 循环中,let和const都会每次迭代时创建新绑定,从而使循环体内创建的函数可以访问到相应迭代的值,而非最后一次迭代后的值(像使用 var 那样)。但在 for 循环中使用const声明则可能引发错误。

posted @ 2021-09-09 14:27  WuMiaoMiao  阅读(91)  评论(0编辑  收藏  举报