若是有人问你:知道 let 和 const 吗 答案是确定的,还能回答的头头是道:javascript
再问:前端
我是一个前端菜鸟,常用 let、const,我知道 let (let 和 const 的基本特性一致,后面就不带 const 了) 不存在变量提高,变量操做必须在声明以后,不然就会报错;还知道 let 声明的变量存在于块级做用域中,块外没法访问块内使用 let 声明的变量;我还知道 let 声明的变量不会被挂载到全局对象中(刚知道);java
好久之前,我知道了执行上下文中的变量对象这个东西,理解了变量对象,继而理解了变量提高的过程,后来心中一直有一个疑惑,使用 var 声明的变量会被建立在 变量对象中,也就是所谓的变量提高,let 又为何不会变量提高呢;es6
复习变量提高的概念,其中涉及到 JS 的 执行上下文、变量对象 等知识。浏览器
var a = "global";
function bar(a) {
console.log(temp); // undefined
var temp = "local";
}
bar();
复制代码
咱们都知道在 js 中,代码执行时会把当前做用域中的全部的使用 var 声明的变量以及 function 声明的函数提高到做用域的顶部(变量提高声明,不提高赋值;函数提高函数体,不提高调用)。因此咱们在 var 声明变量以前能够访问 temp,结果是 undefined缓存
让咱们从 js 代码的运行角度去看这个过程,以下:函数
<1> 执行流进入全局执行环境post
<2> 建立全局执行环境的变量对象, 并将变量 a 的声明、函数 bar 的声明添加到变量对象中ui
<3> 进入全局代码执行阶段lua
<4> 对变量 a 赋值为 'global'
<5> 遇到 函数 bar 的调用
<6> 进入函数 bar 内部,建立 bar 的执行环境,压入执行环境栈
<7> 一样的,建立函数 bar 执行环境的活动对象,将变量 temp 在 活动对象内建立,key 为 temp,初始化值为 undefined
<8> 进入代码执行阶段,遇到 打印输出 a,此时 a 存在于活动对象中,值为 undefined
<9> 继续执行,为 temp 赋值为 'local'
<10> bar 函数内代码执行完毕,执行环境栈谈出 bar 执行环境,执行流回到 global 执行环境中继续执行...
js 在代码预编译阶段,会建立一个变量对象,变量对象中有一个属性,属性名称为 temp,属性值为 undefined,因此在变量声明以前打印 temp,值为 undefined;这也就是 var 的变量提高规则
若是你能理解 var 的提高规则,那么理解 let 的提高将会变得很轻松。 在 MDN 关于 let 的文档中,有这么一句话:
The other difference between var and let is that the latter is initialized to value only when parser evaluates it (see below).
ps: 个人英语极差,MDN 的翻译是这样的:
var 和 let 的不一样之处在于后者是在编译时才初始化(见下面)
我使用 有道翻译 是酱紫的:
var 和 let 之间的另外一个区别是,后者只有在解析器对其求值时才初始化为 value(参见下面的内容)。
我预感到 MDN 的中文翻译并不许确(由于 var 是在编译时初始化的,可见翻译有问题),再结合 有道的硬核翻译,我将其理解为:
var 和 let 的不一样之处在于后者是在运行时才初始化的
在 MDN 上点击 (见下面) 看到了 暂存死区的概念:
与经过 var 声明的有初始化值 undefined 的变量不一样,经过 let 声明的变量直到它们的定义被执行时才初始化。在变量初始化前访问该变量会致使 ReferenceError。该变量处在一个自块顶部到初始化处理的“暂存死区”中。
function foo() {
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 123;
}
foo();
复制代码
典型的 let 声明变量的例子:
与 var 不一样的是,咱们没法在 let 声明以前获取该变量;
不然,报错:Cannot access 'a' before initialization;
报错信息给到咱们,硬核翻译为:没法在初始化以前访问'a';
从 “没法在初始化以前访问'a'” 是否可以看出写端倪呢?
let a = 3;
let b;
(function() {
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
console.log(b);
let a = (b = 3);
console.log(a);
console.log(b);
})();
console.log(a);
console.log(b);
复制代码
结果为:报错 - Uncaught ReferenceError: Cannot access 'a' before initialization;
与 例 1 报的一样的错误;
咱们试想下:
在匿名函数内,第一个访问的 a,为何不能获取到外部环境的 a = 3;而是在这里就早早的报错了呢?
估计不少人就会说:这不就是由于 let 的暂时性死区特性嘛;
在 阮大神的 《es6 入门》 中,对于暂时性死区这样写到:
只要块级做用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,再也不受外部的影响。
以及后面的介绍,在我看来只是声明式的告诉了我:在做用域内,不管代码前后顺序,只要在代码中出现了 let a,那么 a 变量就 “绑定” 了这个做用域,有没有疑问? 为何我声明变量的代码明明还在下面,上面的代码就被绑定了呢,js 不是顺序执行的吗?
OK,走到了如今,结合 var 的提高规则,是否是能够将 let 理解为:
- 使用 let 声明的变量实际上也存在提高,可是与 var 的提高规则不一样:
- var 声明的变量是在代码预编译阶段 被建立在了 执行环境的变量对象中,而且将其初始化,初始化值为 undefined;
- 而 let 声明的变量,在执行环境预编译阶段,被提高到了一个叫作 “暂存死区” 的地方,而且没有对其进行初始化;
- 因此,不管使用 let 声明的变量,声明在代码的任何位置,这个变量一开始就被 “绑定” 在了当前执行环境中;
- 在进入执行阶段时,只有在 let 声明被执行后,才对这个变量进行了初始化;
- 因此,在执行到 let 声明以前,是没法访问这个被 “绑定” 的变量的。
let a = 3;
let b;
(function() {
let a;
console.log(a); // undefined
console.log(b); // undefined
b = 3;
a = b;
console.log(a); // 3
console.log(b); // 3
})();
console.log(a); // 3
console.log(b); // 3
复制代码
代码顺利执行
咱们将变量 a 的声明提高到了函数的顶部,在进入函数执行阶段,第一句执行的就是 let a; 声明了 a 变量,并将其值初始化为 undefined
因此在下面打印 a 为 undefined
后续代码就不说了...
- 其实 let 和 const 也是存在变量提高的,只不过和 var 的提高规则不一样,let 是将变量提高到了一个叫作 “暂存死区” 的地方,在提高时并无对其进行初始化,若是去访问 “暂存死区” 中的变量,就会报错;
- 因为 let 的变量提高是在 “预编译” 阶段完成的,因此在进入执行环境后,不管声明代码在何处,这个被声明的变量就被绑定了,在声明以前访问这个变量,就会报错,也就会造成 代码的 “暂时性死区”
- 在执行环境的执行阶段,在当执行到 let 声明时,那个变量才会 从 “暂存死区” 中移除,并对齐初始化为 undefined,因此 使用 let 声明的变量只能在其声明后访问。
- 其实在个人理解中,“暂存死区” 和 “暂时性死区” 并非一个概念,“暂存死区” 亦可称为 “死缓区” (死亡缓存区域? 随便怎么叫了),“暂存死区” 是一个保存 let 声明的变量的地方,正是由于有这个区域的存在,才使得 let 声明的变量可以保证不能再声明前访问变量,继而也就造成了 “暂时性死区”
在 《es6 入门》 中如是写到:
顶层对象,在浏览器环境指的是 window 对象,在 Node 指的是 global 对象。ES5 之中,顶层对象的属性与全局变量是等价的。
let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。
在 es5 时期,全局声明的变量是被挂载在顶级对象下的,在 es6 时期,使用 let 声明的全局变量,并不存在于顶级对象下
那么,使用 let 声明的变量存在于哪里呢?
var a = 1;
let b = 2;
console.log(window.a); // 1
console.log(window.b); // undefined
复制代码
下列涉及到做用域、做用域链生成规则等知识, 若是不理解做用链的生成规则可能会对下面产生疑惑。
let str = "global Str";
function bar() {
let str = "local Str";
}
console.dir(bar);
复制代码
输出结果:
上图中的 圈红部分是函数的做用域链(在 谷歌浏览器能够看到如上的打印输出,在 火狐 ie 下均没有)
能够看到,在做用域链中存在两项:
[[Scopes]][0]: Script
和顶级对象平行的一个做用域,能够看到里面貌似有熟悉的东西[[Scopes]][1]: Global
也就是顶级做用域的变量对象,在浏览器中就是 window
let 定义的全局变量并不存在于 顶级对象中,而是存在于和顶级对象平行的一个全局做用域中
亦或者能够说 let 和 const 定义的变量是存在于做用链的顶端的,根据做用域链的访问规则,能够访问到全局变量
至于这个 做用域链顶端的 Script 究竟是什么,我也说不清楚,我目前没有仔细去找解释这个
[[Scopes]][0]: Script
的文档,大体的找了下,并无找到,还望有了解的大佬解释下这个 做用域链 顶端的 Script 究竟是个什么东西。