Blog icon indicating copy to clipboard operation
Blog copied to clipboard

ES6 系列之 let 和 const

Open mqyqingfeng opened this issue 6 years ago • 71 comments

块级作用域的出现

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

mqyqingfeng avatar May 21 '18 03:05 mqyqingfeng

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

BeijiYang avatar May 21 '18 09:05 BeijiYang

写的很不错

On Mon, May 21, 2018 at 5:59 PM BeijiYang [email protected] wrote:

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

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/mqyqingfeng/Blog/issues/82#issuecomment-390609929, or mute the thread https://github.com/notifications/unsubscribe-auth/AB212IEY7zdOT5R-HRXTU5ECl7wuAbZOks5t0pARgaJpZM4UGXxM .

shadowprompt avatar May 21 '18 14:05 shadowprompt

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

CharlyCheng avatar May 22 '18 03:05 CharlyCheng

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

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

brushbird avatar May 23 '18 01:05 brushbird

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

thisisandy avatar May 23 '18 02:05 thisisandy

非常赞。

horizon0514 avatar May 23 '18 03:05 horizon0514

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

mqyqingfeng avatar May 31 '18 13:05 mqyqingfeng

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

mqyqingfeng avatar May 31 '18 13:05 mqyqingfeng

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

mqyqingfeng avatar May 31 '18 13:05 mqyqingfeng

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

heyunjiang avatar Jun 06 '18 03:06 heyunjiang

我就是来刷刷存在感滴

liuxinqiong avatar Jun 07 '18 01:06 liuxinqiong

@brushbird 'abc'<3 -> false

Nealyang avatar Jun 08 '18 07:06 Nealyang

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

Young-Young-Young avatar Jun 15 '18 06:06 Young-Young-Young

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

heyunjiang avatar Jun 15 '18 09:06 heyunjiang

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

default

mqyqingfeng avatar Jun 19 '18 14:06 mqyqingfeng

@heyunjiang ( ̄▽ ̄)~*

mqyqingfeng avatar Jun 19 '18 14:06 mqyqingfeng

@liuxinqiong ( ̄∇ ̄)

mqyqingfeng avatar Jun 19 '18 14:06 mqyqingfeng

有两个疑问: 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,又是为什么?

whwiGrado avatar Jul 03 '18 13:07 whwiGrado

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

cobish avatar Jul 12 '18 12:07 cobish

Babel 巧妙的编译成了

应该为巧妙地

WangNianyi2001 avatar Jul 28 '18 09:07 WangNianyi2001

有两个疑问: 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 avatar Aug 04 '18 08:08 CharlyCheng

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

CharlyCheng avatar Aug 04 '18 08:08 CharlyCheng

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

zjhch123 avatar Sep 18 '18 08:09 zjhch123

@lzy68187311 image

slogeor avatar Oct 09 '18 00:10 slogeor

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

zhixinpeng avatar Oct 19 '18 02:10 zhixinpeng

我的项目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"
  ]
}

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

zalatmza avatar Nov 20 '18 07:11 zalatmza

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

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

HuangQiii avatar Nov 21 '18 16:11 HuangQiii

nice 马飞

zjp6049 avatar Nov 27 '18 06:11 zjp6049

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

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有什么区别呢,感觉看起来一样,但是答案似乎有出入...

kuangjiajia avatar Dec 13 '18 02:12 kuangjiajia

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

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

因为for( )里面使用var时没有在( )内形成封闭的作用域,i被循环体赋值成'abc',!!('abc' < 3) === false,循环停止了。

woshiqiang1 avatar Dec 28 '18 01:12 woshiqiang1