ES6 系列之 let 和 const

块级做用域的出现

经过 var 声明的变量存在变量提高的特性:git

if (condition) {
    var value = 1;
}
console.log(value);

初学者可能会以为只有 condition 为 true 的时候,才会建立 value,若是 condition 为 false,结果应该是报错,然而由于变量提高的缘由,代码至关于:github

var value;
if (condition) {
    value = 1;
}
console.log(value);

若是 condition 为 false,结果会是 undefined。面试

除此以外,在 for 循环中:闭包

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

即使循环已经结束了,咱们依然能够访问 i 的值。异步

为了增强对变量生命周期的控制,ECMAScript 6 引入了块级做用域。函数

块级做用域存在于:oop

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

let 和 const

块级声明用于声明在指定块的做用域以外没法访问的变量。lua

let 和 const 都是块级声明的一种。spa

咱们来回顾下 let 和 const 的特色:code

1.不会被提高

if (false) {
    let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined

2.重复声明报错

var value = 1;
let value = 2; // Uncaught SyntaxError: Identifier 'value' has already been declared

3.不绑定全局做用域

当在全局做用域中使用 var 声明的时候,会建立一个新的全局变量做为全局对象的属性。

var value = 1;
console.log(window.value); // 1

然而 let 和 const 不会:

let value = 1;
console.log(window.value); // undefined

再来讲下 let 和 const 的区别:

const 用于声明常量,其值一旦被设定不能再被修改,不然会报错。

值得一提的是:const 声明不容许修改绑定,但容许修改值。这意味着当用 const 声明对象时:

const data = {
    value: 1
}

// 没有问题
data.value = 2;
data.num = 3;

// 报错
data = {}; // Uncaught TypeError: Assignment to constant variable.

临时死区

临时死区(Temporal Dead Zone),简写为 TDZ。

let 和 const 声明的变量不会被提高到做用域顶部,若是在声明以前访问这些变量,会致使报错:

console.log(typeof value); // Uncaught ReferenceError: value is not defined
let value = 1;

这是由于 JavaScript 引擎在扫描代码发现变量声明时,要么将它们提高到做用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,而后方可访问。

看似很好理解,不保证你不犯错:

var value = "global";

// 例子1
(function() {
    console.log(value);

    let value = 'local';
}());

// 例子2
{
    console.log(value);

    const value = 'local';
};

两个例子中,结果并不会打印 "global",而是报错 Uncaught ReferenceError: value is not defined,就是由于 TDZ 的缘故。

循环中的块级做用域

var funcs = [];
for (var i = 0; i < 3; i++) {
    funcs[i] = function () {
        console.log(i);
    };
}
funcs[0](); // 3

一个老生常谈的面试题,解决方案以下:

var funcs = [];
for (var i = 0; i < 3; i++) {
    funcs[i] = (function(i){
        return function() {
            console.log(i);
        }
    }(i))
}
funcs[0](); // 0

ES6 的 let 为这个问题提供了新的解决方法:

var funcs = [];
for (let i = 0; i < 3; i++) {
    funcs[i] = function () {
        console.log(i);
    };
}
funcs[0](); // 0

问题在于,上面讲了 let 不提高,不能重复声明,不能绑定全局做用域等等特性,但是为何在这里就能正确打印出 i 值呢?

若是是不重复声明,在循环第二次的时候,又用 let 声明了 i,应该报错呀,就算由于某种缘由,重复声明不报错,一遍一遍迭代,i 的值最终仍是应该是 5 呀,还有人说 for 循环的
设置循环变量的那部分是一个单独的做用域,就好比:

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

这个例子是对的,若是咱们把 let 改为 var 呢?

for (var i = 0; i < 3; i++) {
  var i = 'abc';
  console.log(i);
}
// abc

为何结果就不同了呢,若是有单独的做用域,结果应该是相同的呀……

若是要追究这个问题,就要抛弃掉以前所讲的这些特性!这是由于 let 声明在循环内部的行为是标准中专门定义的,不必定就与 let 的不提高特性有关,其实,在早期的 let 实现中就不包含这一行为。

咱们查看 ECMAScript 规范第 13.7.4.7 节:

let 规范

咱们会发现,在 for 循环中使用 let 和 var,底层会使用不一样的处理方式。

那么当使用 let 的时候底层究竟是怎么作的呢?

简单的来讲,就是在 for (let i = 0; i < 3; i++) 中,即圆括号以内创建一个隐藏的做用域,这就能够解释为何:

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

而后每次迭代循环时都建立一个新变量,并以以前迭代中同名变量的值将其初始化。这样对于下面这样一段代码

var funcs = [];
for (let i = 0; i < 3; i++) {
    funcs[i] = function () {
        console.log(i);
    };
}
funcs[0](); // 0

就至关于:

// 伪代码
(let i = 0) {
    funcs[0] = function() {
        console.log(i)
    };
}

(let i = 1) {
    funcs[1] = function() {
        console.log(i)
    };
}

(let i = 2) {
    funcs[2] = function() {
        console.log(i)
    };
};

当执行函数的时候,根据词法做用域就能够找到正确的值,其实你也能够理解为 let 声明模仿了闭包的作法来简化循环过程。

循环中的 let 和 const

不过到这里尚未结束,若是咱们把 let 改为 const 呢?

var funcs = [];
for (const i = 0; i < 10; i++) {
    funcs[i] = function () {
        console.log(i);
    };
}
funcs[0](); // Uncaught TypeError: Assignment to constant variable.

结果会是报错,由于虽然咱们每次都建立了一个新的变量,然而咱们却在迭代中尝试修改 const 的值,因此最终会报错。

说完了普通的 for 循环,咱们还有 for in 循环呢~

那下面的结果是什么呢?

var funcs = [], object = {a: 1, b: 1, c: 1};
for (var key in object) {
    funcs.push(function(){
        console.log(key)
    });
}

funcs[0]()

结果是 'c';

那若是把 var 改为 let 或者 const 呢?

使用 let,结果天然会是 'a',const 呢? 报错仍是 'a'?

结果是正确打印 'a',这是由于在 for in 循环中,每次迭代不会修改已有的绑定,而是会建立一个新的绑定。

Babel

在 Babel 中是如何编译 let 和 const 的呢?咱们来看看编译后的代码:

let value = 1;

编译为:

var value = 1;

咱们能够看到 Babel 直接将 let 编译成了 var,若是是这样的话,那么咱们来写个例子:

if (false) {
    let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined

若是仍是直接编译成 var,打印的结果确定是 undefined,然而 Babel 很聪明,它编译成了:

if (false) {
    var _value = 1;
}
console.log(value);

咱们再写个直观的例子:

let value = 1;
{
    let value = 2;
}
value = 3;
var value = 1;
{
    var _value = 2;
}
value = 3;

本质是同样的,就是改变量名,使内外层的变量名称不同。

那像 const 的修改值时报错,以及重复声明报错怎么实现的呢?

其实就是在编译的时候直接给你报错……

那循环中的 let 声明呢?

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

Babel 巧妙的编译成了:

var funcs = [];

var _loop = function _loop(i) {
    funcs[i] = function () {
        console.log(i);
    };
};

for (var i = 0; i < 10; i++) {
    _loop(i);
}
funcs[0](); // 0

最佳实践

在咱们开发的时候,可能认为应该默认使用 let 而不是 var ,这种状况下,对于须要写保护的变量要使用 const。然而另外一种作法日益普及:默认使用 const,只有当确实须要改变变量的值的时候才使用 let。这是由于大部分的变量的值在初始化后不该再改变,而预料以外的变量之的改变是不少 bug 的源头。

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfen...

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级做用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

若是有错误或者不严谨的地方,请务必给予指正,十分感谢。若是喜欢或者有所启发,欢迎star,对做者也是一种鼓励。

相关文章
相关标签/搜索