Skip to content

ES6 系列之 let 和 const #82

Description

@mqyqingfeng

块级作用域的出现

通过 var 声明的变量存在变量提升的特性:

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

初学者可能会觉得只有 condition 为 true 的时候,才会创建 value,如果 condition 为 false,结果应该是报错,然而因为变量提升的原因,代码相当于:

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 引入了块级作用域。

块级作用域存在于:

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

let 和 const

块级声明用于声明在指定块的作用域之外无法访问的变量。

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

我们来回顾下 let 和 const 的特点:

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 的值最终还是应该是 3 呀,还有人说 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/mqyqingfeng/Blog

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

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

Activity

  1. BeijiYang commented on May 21, 2018

    @BeijiYang

    哈,还是先写了 ES6 ,手动 DOGE。

  2. shadowprompt commented on May 21, 2018

    @shadowprompt
  3. CharlyCheng commented on May 22, 2018

    @CharlyCheng

    期待V8源码系列已经很久了,我的大刀已经饥渴难耐了!

  4. brushbird commented on May 23, 2018

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

    这里为什么会只输出一个abc呢??求教

  5. thisisandy commented on May 23, 2018

    @thisisandy

    @brushbird
    'abc' ++ --> NaN
    NAN<3 --->False
    循环退出

  6. horizon0514 commented on May 23, 2018

    @horizon0514

    非常赞。

  7. mqyqingfeng commented on May 31, 2018

    @mqyqingfeng
    OwnerAuthor

    @BeijiYang 写完 ES6 系列,等我写 React 系列的时候,就可以尽情的使用 ES6 的语法 (๑•̀ㅂ•́)و✧

  8. mqyqingfeng commented on May 31, 2018

    @mqyqingfeng
    OwnerAuthor

    @CharlyCheng 这个……不是想让你伤心……但我没有说过写 V8 系列……

  9. mqyqingfeng commented on May 31, 2018

    @mqyqingfeng
    OwnerAuthor

    @shadowprompt @thisisandy @horizon0514 @JChermy 感谢你们,送上致意 o(////▽////)q

  10. heyunjiang commented on Jun 6, 2018

    @heyunjiang

    let 在 for 循环中创建的隐藏作用域,以及babel模拟创建函数作用域,用以保存i的值,让我印象深刻呀。以前只知道for循环中let可以解决var变量问题,现在知道原理了

  11. liuxinqiong commented on Jun 7, 2018

    @liuxinqiong

    我就是来刷刷存在感滴

  12. Nealyang commented on Jun 8, 2018

    @Nealyang

    @brushbird 'abc'<3 -> false

  13. Young-Young-Young commented on Jun 15, 2018

    @Young-Young-Young

    博主有个问题不太理解~
    for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); }
    babel 编译后为
    for (var i = 0; i < 3; i++) { var i = "abc"; console.log(i); }
    这快我有点不理解 编译前后的代码 执行行为应该是一致的 现在却不一致.

  14. heyunjiang commented on Jun 15, 2018

    @heyunjiang

    @lzy68187311 你确定编译后是这个样子?应该不是吧

  15. 49 remaining items

  16. wweggplant commented on Aug 6, 2020

    @wweggplant

    @617429782 在for循环里有特殊处理,可以搜一下知乎方应杭的写的一篇文章,名字我记得大概是终于理解了let

  17. MoJiaBings commented on Aug 19, 2020

    @MoJiaBings

    react啥时候出大佬

  18. lwangwang commented on Sep 6, 2020

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

    这里为什么会只输出一个abc呢??求教

    var i=abc
    你说呢

  19. jxccc1998 commented on Jan 11, 2021

    @jxccc1998

    请问一下,tdz具体是个啥东西,是会专门开辟一段内存去储存let const这些吗

  20. yyang755 commented on Mar 18, 2021

    @yyang755

    块级作用域存在于 “块中(字符 { 和 } 之间的区域)”, 这句怎么理解呀

  21. jianzhou520 commented on Mar 9, 2022

    @jianzhou520

    function init () {
    console.log('init called');
    return Math.random();
    }

    for (let i = 0, j = init(); i < 100; i++) {
    console.log(i);
    console.log(j);
    }

    // 最后输出的j都是完全一样的,for循环的第一段变量声明应该只会执行一次,但是会单独开辟一个块级作用域

  22. isJx commented on Sep 1, 2022

    @isJx

    image

    有个错别字

  23. btqf commented on Feb 28, 2023

    @btqf

    解决了之前一些模棱两可的问题,感谢~

  24. liuxiaoru666 commented on Aug 12, 2024

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

    这里为什么会只输出一个abc呢??求教

    因为i在第一次执行之后,被改写成了‘abc’,不符合后续循环条件了

  25. btqf commented on Aug 12, 2024

    @btqf
  26. rainbowyy commented on Aug 12, 2024

    @rainbowyy
  27. zhenghan2017 commented on May 7, 2025

    @zhenghan2017

    关于临死性死区最近有碰到一个问题,仔细研究了一下,let 和 const 也是会存在提升的,只是他提升之后的值并不是基础数据类型,而是内部定义的 uninitialized,当访问到的值为该值时抛出错误。

    zhenghan2017/daydayup#1

  28. rainbowyy commented on May 7, 2025

    @rainbowyy
  29. btqf commented on May 7, 2025

    @btqf
  30. loveheavenlina commented on May 7, 2025

    @loveheavenlina
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions