Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6 系列之 let 和 const #82

Open
mqyqingfeng opened this issue May 21, 2018 · 71 comments
Open

ES6 系列之 let 和 const #82

mqyqingfeng opened this issue May 21, 2018 · 71 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented May 21, 2018

块级作用域的出现

通过 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,对作者也是一种鼓励。

@BeijiYang
Copy link

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

@shadowprompt
Copy link

shadowprompt commented May 21, 2018 via email

@CharlyCheng
Copy link

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

@brushbird
Copy link

brushbird commented May 23, 2018

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

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

@thisisandy
Copy link

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

@horizon0514
Copy link

非常赞。

@mqyqingfeng
Copy link
Owner Author

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

@mqyqingfeng
Copy link
Owner Author

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

@mqyqingfeng
Copy link
Owner Author

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

@heyunjiang
Copy link

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

@liuxinqiong
Copy link

我就是来刷刷存在感滴

@Nealyang
Copy link

Nealyang commented Jun 8, 2018

@brushbird 'abc'<3 -> false

@Young-Young-Young
Copy link

博主有个问题不太理解~
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); }
这快我有点不理解 编译前后的代码 执行行为应该是一致的 现在却不一致.

@heyunjiang
Copy link

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

@mqyqingfeng
Copy link
Owner Author

@lzy68187311 如果是 babel 官网的那个 try out 确实是编译成这个样子的,但是我在项目中编译了一下,Babel 很机智的编译成了:

default

@mqyqingfeng
Copy link
Owner Author

@heyunjiang ( ̄▽ ̄)~*

@mqyqingfeng
Copy link
Owner Author

@liuxinqiong ( ̄∇ ̄)

@whwiGrado
Copy link

有两个疑问:
var value = "global";

// 例子1
(function() {
debugger // 在断点处访问 value,返回的是 undefined
console.log(value);
let value = 'local';
}());

这个例子中,自执行函数中
1、先执行 console.log(value),此时还没有执行 let value = 'local';
那么 value 是怎么被放入 TDZ 的呢?
2、如果加上了 debugger,在断点处访问 value,返回的是 undefined,又是为什么?

@cobish
Copy link

cobish commented Jul 12, 2018

@JChermy me, jd sz, too. 同事呀

@WangNianyi2001
Copy link

Babel 巧妙的编译成了

应该为巧妙地

@CharlyCheng
Copy link

有两个疑问:
var value = "global";

// 例子1
(function() {
debugger // 在断点处访问 value,返回的是 undefined
console.log(value);
let value = 'local';
}());

这个例子中,自执行函数中
1、先执行 console.log(value),此时还没有执行 let value = 'local';
那么 value 是怎么被放入 TDZ 的呢?
2、如果加上了 debugger,在断点处访问 value,返回的是 undefined,又是为什么?
同问临时死区的概念分析?

@CharlyCheng
Copy link

@mqyqingfeng ,js 深入系列时,有人要说分析V8源码来,好像后来再看,已经从入门到放弃了

@zjhch123
Copy link

@CharlyCheng js引擎会先扫描整个代码,在自执行函数作用域内扫描到了变量定义(let value = 'local'),导致value被加入死区。之后执行过程中在函数作用域内访问value,则报错

@slogeor
Copy link

slogeor commented Oct 9, 2018

@lzy68187311
image

@zhixinpeng
Copy link

不要V8,但说好的React系列呢。。

@zalatmza
Copy link

我的项目babel编译下面这段代码也有问题

// 编译前
for (let i = 0; i<3;i++) {
  let i = 10
  console.log(i) //10,10,10
}

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

全局安装的[email protected],以及项目下安装的babel-preset-env@^1.7.0, babel-preset-stage-2@^6.24.1

//.babelrc
{
  "presets": [
    ["env", {
      "targets": {
        "browsers": ["last 3 versions", "> 2%", "ie >= 9", "Firefox >= 30", "Chrome >= 30"]
      },
      "modules": false,
      "loose": true,
      "useBuiltIns": true
    }],
    "stage-2"
  ]
}

有人遇到和我一样的问题么

@HuangQiii
Copy link

babel处理for中的let的方法,可以说是一种非常优雅的解决办法了

小鸡蛋里挑骨头:
最后一段而预料之外的变量*之*的改变是很多 bug 的源头,错别字🤭

@zjp6049
Copy link

zjp6049 commented Nov 27, 2018

nice 马飞

@kuangjiajia
Copy link

大佬 就是我遇到一个解构赋值的地方想请教一下

function ListNode(val) {
  this.val = val;
  this.next = null;
}

var head = new ListNode(1)
var tmp = head


for (var i = 2; i < 5; i++) {

  head.next = new ListNode(i)
  head = head.next
}

// console.log(tmp)

var swapPairs = function (head) {
  // console.log(head)
  if (!head || !head.next) return head
  let tmp = head.next
  console.log(tmp)
  [head.next, tmp.next] = [swapPairs(tmp.next), head] // 1
  // head.next = swapPairs(tmp.next) //2
  // tmp.next = head //3
  return tmp
}

console.log(swapPairs(tmp))

1 和 2+3有什么区别呢,感觉看起来一样,但是答案似乎有出入...

@chufengge
Copy link

一次循环后i = 'abc' 已经不满足 i < 3的条件啦 @wudixiaodoujie

@bingyuea
Copy link

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

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

上面不是有人已经说过了吗,i = abc ,然后再++ 转化成 NaN ,小于 3

@law-chain-hot
Copy link

突发奇想,babel如何解决命名冲突,因为babel在for循环中使用let,会生成一个叫做 _loop 的变量,但是,如果把这个变量提前声明占位会发生什么呢? 结论是 _loop 会变为 _loop2

正常情况

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


// 编译后Babel case
var funcs = [];

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

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

不正常情况

下面为提前申明 _loop,原来的本该变为_loop的变量,此时命名为_loop2

// 编译前Original case
var funcs = [];
var _loop = 1; //提前定义 _loop 占位,看babel如何解决冲突

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



// 编译后Babel case
var funcs = [];
var _loop = 1;  //提前申明的变量依旧在

// 原来的本该变为_loop的变量,此时命名为_loop2
var _loop2 = function _loop2(i) { 
  funcs[i] = function () {
    console.log(i);
  };
};

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

@Alex-Li2018
Copy link

感谢楼主

@ZhangDaZongWei
Copy link

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

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

上面不是有人已经说过了吗,i = abc ,然后再++ 转化成 NaN ,小于 3

我也是这样想的,但是当执行"abc"++的时候,会报错的啊,并不能得出NaN

@ZhangDaZongWei
Copy link

ZhangDaZongWei commented Apr 7, 2020

@brushbird 'abc'<3 -> false

当执行完var i = "abc" console.log(i)时,下一步应该会执行i++吧,那终止的条件不能是"abc" < 3 -> false 啊。验证了一下,是在i++这一步终止循环。按理说执行"abc"++时会报错的,怎么没有呢🤔

@johe-a
Copy link

johe-a commented Apr 7, 2020

js里面的所有声明都是有提升的,也是看过了您的关于变量对象的文章才得出此结论,提升的本质应该是当前变量在变量对象初始化时,创建于变量对象中。把变量对象的变化过程分为三个步骤,第一步是创建,第二步是初始化,第三步是赋值。对于var/class/function/形参等创建和初始化在变量对象的初始化阶段,对于function、class、形参等赋值也在变量对象的初始化阶段。而let、const在变量对象的初始化阶段是被创建但未初始化的。

@Randysheng
Copy link

@ZhangDaZongWei 你好,在 for 循环中执行的代码是 var i = "abc"; i++。而你直接是把字符串带入 到++ 操作了,这两者是不一样的。报错是因为 ++ 操作符只能用在左值上吧,也就是变量上。例如:i++ 其实等价于 i = i + 1。若咱们直接使用 "abc"++,那就等同于 "abc" = "abc" +1,但是 "abc" 是一个字符串常量,它不能放在 = 左边的。所以直接在浏览器上执行 "abc"++ 是会报错的。个人理解,请多指教~

@Bittttter
Copy link

Bittttter commented Apr 24, 2020

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

这句话我觉得有一丝歧义,const 其实就是不可修改值,比如一个对象

const obj = {
name: ''
}

obj 指向的是变量在内存中的地址,这个地址(也就是const声明的标识符的值)不可以被修改,但是这个地址指向的内存空间,也就是name所在的空间所存储的值,是可以随意修改的。

@websrookie
Copy link

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

这句话我觉得有一丝歧义,const 其实就是不可修改值,比如一个对象

const obj = {
name: ''
}

obj 指向的是变量在内存中的地址,这个地址(也就是const声明的标识符的值)不可以被修改,但是这个地址指向的内存空间,也就是name所在的空间所存储的值,是可以随意修改的。

其实这个就是跟按值传递同一个类型的问题

@G96968586
Copy link

image

我觉得 let const 声明的变量是会提升的,只是没有初始化,所以访问报错(未初始化前不能访问)

可以这么理解:
function test() {
// TDZ 开始
color = "yellow";
let color = "red"; // TDZ 结束,在 let 命令声明变量 color 之前,都属于变量 color 的“死区”。
console.log(color);
}
所以,color = "yellow" 这里就会报错了。我一开始以为报错是在打印语句那,懵逼了半天。
至于为什么有人说还是会变量提升,其实是受到了块级作用域的干扰。在块级作用域和在全局作用域下,下面的代码表现会不一致,如下:
image
在全局作用域下,的确是不存在变量提升的,在块级作用域下,貌似是存在类似于变量提升的现象,区别在于未初始化不能访问。

@sunseekers
Copy link

react 的什么时候出呀

@wweggplant
Copy link

wweggplant commented Jun 23, 2020

关于 let 提升的问题,可以看看这篇方老大的总结, 至于结论,没有这篇文章精彩

@itagan
Copy link

itagan commented Jul 4, 2020

声明

var变量函数提升放到变量环境,let const放在词法环境。想多了解看yygmind/blog#12
其他资料,极客时间 浏览器原理与实践专栏

@puck1006
Copy link

@brushbird 'abc'<3 -> false

当执行完var i = "abc" console.log(i)时,下一步应该会执行i++吧,那终止的条件不能是"abc" < 3 -> false 啊。验证了一下,是在i++这一步终止循环。按理说执行"abc"++时会报错的,怎么没有呢🤔

i='abc'
i++ //NAN

@617429782
Copy link

求教 for (let i = 0; i < 3; i++) { console.log(i) } 的伪代码应该是什么样子,如果按这个形式
{ let i = 0 { console.log(i) } i++ { console.log(i) } i++ { console.log(i) } i++ }
可以解释一些问题,比如
(1) 循环体内可以重复声明循环条件中定义的变量
for (let i = 0; i < 3; i++) { let i = 1 }
// 不报错, 循环条件 与 循环体 并非同一个作用域,而是嵌套关系
(2) 循环体内可以改变循环条件中的变量
for (let i = 0; i < 3; i++) { i = 10; console.log(i) }
// 只执行了第一次,执行体内对变量的修改影响了执行条件中的同名变量,再次证明嵌套关系
(3) const 无法声明循环条件的变量
for (const i = 0; i < 3; i++) { console.log(i) }
// 由此可见循环条件内存在对变量 i 对修改,即 i++
但是无法解释以下情况
(4) for (let i = 0; i < 3; i++) { setTimeout(() => { console.log(i) }, 0) }

@wweggplant
Copy link

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

@MoJiaBings
Copy link

react啥时候出大佬

@lwangwang
Copy link

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

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

var i=abc
你说呢

@jxccc1998
Copy link

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

@yyang755
Copy link

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

@jianzhou520
Copy link

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循环的第一段变量声明应该只会执行一次,但是会单独开辟一个块级作用域

@isJx
Copy link

isJx commented Sep 1, 2022

image

有个错别字

@btqf
Copy link

btqf commented Feb 28, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests