说到ES6的let
变量声明,我估计不少人会想起下面几个主要的特色:javascript
不少教程和总结基本都说到了这几点(说实话大部分文章都大同小异,摘录的居多),习惯性我仍是去看了MDN上的文档,立马发现一个问题:html
In ECMAScript 2015, let will hoist the variable to the top of the block. However, referencing the variable in the block before the variable declaration results in a ReferenceError. The variable is in a "temporal dead zone" from the start of the block until the declaration is processed.java
ECMAScript 2015(即ES6),let会提高变量到代码块顶部。然而,在变量声明前引用变量会致使ReferenceError错误。在代码块开始到变量声明之间变量处于暂时死区(temporal dead zone)。
不得了,看来let是有变量声明提高的啊,这个发现引发了个人兴趣。我立马去找了一些相关的资料查看,在查看的过程当中,我也慢慢了解了其余一些隐含的容易误解的知识点,下面罗列一些相关资料,方便让有一样兴趣了解的童鞋去查阅:node
不肯意去翻阅资料的就看我下面的我的总结吧。git
关于变量声明提高,有几个重点:es6
在个人思路大概清晰写这篇总结的时候,我又偶然在一篇讲变量声明提高的博文上看到一段MDN原文的引用:github
In ECMAScript 6, let does not hoist the variable to the top of the block. If you reference a variable in a block before the let declaration for that variable is encountered, this results in a ReferenceError, because the variable is in a "temporal dead zone" from the start of the block until the declaration is processed.数组
纳尼!竟然和我如今看到的MDN文档不一致......博文的日期是2015-06-11,看来这个概念也在改变,与时俱进啊。既然如此,我以为也没有必要深究了,由于无论概念怎么变,只要可以知道let在块级做用域的正确表现就能够了,理论仍是要为实践服务。闭包
说到for循环,先说明下for的运行机制,好比说for(var i=0;i<10;i++){...}即先初始化循环变量(var i=0),这一句只运行一次,而后进行比较(i<10),而后运行函数体{...},函数体运行结束后,若是没有break等跳出,再运行自增表达式(i++),而后进行比较判断(i<10)是否进入执行体。下面是引用别人的一个回答How are for loops executed in javascript?,将这个过程描述得很清晰:ide
// for(initialise; condition; finishediteration) { iteration } var initialise = function () { console.log("initialising"); i=0; } var condition = function () { console.log("conditioning"); return i<5; } var finishediteration = function () { console.log("finished an iteration"); i++; } var doingiteration = function () { console.log("doing iteration when `i` is equal", i); } for (initialise(); condition(); finishediteration()) { doingiteration(); } initialising conditioning doing iteration when `i` is equal 0 finished an iteration conditioning doing iteration when `i` is equal 1 finished an iteration conditioning doing iteration when `i` is equal 2 finished an iteration conditioning doing iteration when `i` is equal 3 finished an iteration conditioning doing iteration when `i` is equal 4 finished an iteration conditioning
之因此要单独讲for循环中的let,是由于看到了阮老师ES6入门中讲let的那一章的一个例子:
var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6
对这个例子原文中是这样的解释的:
上面代码中,变量i是let声明的,当前的i只在本轮循环有效,因此每一次循环的i其实都是一个新的变量,因此最后输出的是6。你可能会问,若是每一轮循环的变量i都是从新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是由于 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
JavaScript 引擎内部会记住上一轮循环的值
这句解释我以为做为程序猿估计怎么都没法承认吧?记住这个词说得太模糊了,其中当然有某种机制或规范。并且每一轮循环的变量i都是从新声明
,那么下面的例子就难以解释:
for (let i = 0; i < 5; i++){ i++; console.log(i) } // 1 // 3 // 5
若是循环函数体内的i
每次都是从新声明的,那么函数体内即子做用域内改变i
的值,为何可以改变外层定义的i
变量?
再来看文中提的另一个例子:
for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); } // abc // abc // abc
这个例子原文的解释是:
循环语句部分是一个父做用域,而循环体内部是一个单独的子做用域。
若是按照上面的逻辑每一个子做用域内的i
都从新声明,那么在同一个子做用域内为何可以二次声明?
很明显,i
并无从新声明。看来咱们有必要借助其余文档来帮助理解。
MDN上的文档,提到for循环中,每进入一次花括号就生成了一个块级域,即每一个循环进入函数体的i
都绑定到了不一样的块级域中,因为是不一样的块级做用域,因此每次进入循环体函数的i
值都相互独立,不是同一个做用域下的值。
ES6 In Depth: let and const文章中是这样解释的:
each closure will capture a different copy of the loop variable, rather than all closures capturing the same loop variable.
每个闭包(即循环体函数)会捕获循环变量的不一样副本,而不是都捕获同一个循环变量。这里说明了循环体函数中的循环变量不是简单的引用,而是一个副本。
You Don't Know JS: Scope & Closures 中的理解:
Not only does let in the for-loop header bind the i to the for-loop body, but in fact, it re-binds it to each iteration of the loop, making sure to re-assign it the value from the end of the previous loop iteration.
let 不只在头部将i
值绑定到for循环体中,事实上,let将i
从新绑定到每一个迭代函数中,并确保将上一次迭代结束的结果从新赋值给i
这里提到的子做用域(for循环的函数体{...}),其实准确地讲叫词法做用域(lexical scope),也被称为静态做用域。简单地讲就是在嵌套的函数组中,内部函数能够访问父做用域的变量和其余资源。
结合上面的几点可知,子做用域内用的仍是外层声明的i
变量,let i = 'abc';
就至关于在子做用域中声明新的变量覆盖了父做用域的变量声明。可是子做用域内引用的这个父做用域变量不是直接引用,而是父做用域变量的一个副本,子做用域修改这个副本时,至关于修改父做用域变量,而父做用域循环变量改变时,不会影响子做用域内的副本变量,加粗的这句解释说实话仍是没能说服我本身,因此我又找到了stackoverflow上的一个回答。
Why is let slower than var in a for loop in nodejs?虽然不是正面回答for循环的问题,可是里面举的一个Babel实现let的例子却能从var的角度来解释这个问题:
"use strict"; (function () { var _loop = function _loop(_j) { _j++; // here's the change inside the new scope setTimeout(function () { console.log("j: " + _j + " seconds"); }, _j * 1000); j = _j; // here's the change being propagated back to maintain continuity }; for (var j = 0; j < 5; j++) { _loop(j); } })();
仔细看这个例子,外层定义的j
变量由形参_j
(这里的形参传值,就是动态做用域)传入了循环体函数_loop()中,进入函数体中后,_j
就至关于他的副本,子做用域能够修改父做用域变量(表如今 j = _j),但_loop()函数执行结束后,父做用域变量j
的修改没法改变_loop()函数中的形参_j
,由于形参_j
只会在_loop()函数执行那一次被赋值,后面外层j
值的修改和他没有关系。回想一下上面的问题,若是内部从新定义了j
值,那么就会覆盖外层传进来的_j
(虽然在这个例子里j
和_j
变量名不同,可是在let声明里实际上是同一个变量名),至关于子做用域定义了本身内部使用的变量,j = _j;
这样的赋值语句也没有意义了,由于这至关于变量本身给本身赋值。
上面这段话是从var实现let的角度来解释,有点拗口。下面说说个人理解,谈谈let变量是怎么处理这个过程的:
for循环每次进入函数体{...}中,都是进入了新的子做用域中,每一个子做用域相互独立,新的子做用域引用(实际是变量复制)父做用域的循环值变量,同时能够修改变量的值且更新父做用域变量,实际表现就和真正引用了父做用域变量同样。反之,父做用域没法访问此复制变量,因此父做用域中变量的改变不会对子做用域中的变量有什么影响。可是若是子做用域中从新声明了此变量名,新的变量就绑定到了子做用域中,变成了子做用域的内部变量,覆盖了父做用域的循环值变量,子做用域对新声明的变量的修改都在子做用域范围内,父做用域一样没法访问此变量。
明白这些概念有时候感受很繁杂,好像有点牛角尖,可是我以为只有掌握正确的理解方向,才可以根据实际状况去推断、读懂代码,也有利于本身写出规范化、易理解的代码。这篇文章的内容依然是我理解思路的一个记录,有点啰嗦,主要为了之后本身概念模糊后可以找到如今思考的思路,因为其中有不少本身的理解,错漏在所不免,也但愿你们读后能给我提出意见和建议。
本文来源:JuFoFu
本文地址:http://www.cnblogs.com/JuFoFu/p/6726359.html
参考文档:
Jason Orendorff . ES6 In Depth: let and const
You-Dont-Know-JS . You Don't Know JS: Scope & Closures
Hammad Ahmed . Understanding Scope in JavaScript
What is the scope of variables in JavaScript?
Why is let slower than var in a for loop in nodejs?
Are variables declared with let or const not hoisted in ES6?