嗨,你知道 let 和 const 吗?

前言

若是有人问你:知道 let 和 const 吗 答案是确定的,还能回答的头头是道:javascript

  • let 和 const 不就是 es6 新增的声明变量的关键字嘛
  • let 是用来声明变量的,const 是用来声明常量的
  • 与 var 不一样,let、const 不存在变量提高
  • 暂时性死区
  • 不绑定顶级对象

再问:前端

  • let 和 const 真的不存在变量提高吗? 为何不存在
  • 暂时性死区又是如何造成的?
  • let、const 声明的全局变量不在 全局对象中,那它存在于哪里呢?

前言 2

我是一个前端菜鸟,常用 let、const,我知道 let (let 和 const 的基本特性一致,后面就不带 const 了) 不存在变量提高,变量操做必须在声明以后,不然就会报错;还知道 let 声明的变量存在于块级做用域中,块外没法访问块内使用 let 声明的变量;我还知道 let 声明的变量不会被挂载到全局对象中(刚知道);java

好久之前,我知道了执行上下文中的变量对象这个东西,理解了变量对象,继而理解了变量提高的过程,后来心中一直有一个疑惑,使用 var 声明的变量会被建立在 变量对象中,也就是所谓的变量提高,let 又为何不会变量提高呢;es6

1. var 的变量提高规则

复习变量提高的概念,其中涉及到 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 的变量提高规则

2. let 的提高规则

死缓区? 暂存死区? TDZ (Temporal dead zone)?

若是你能理解 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。该变量处在一个自块顶部到初始化处理的“暂存死区”中。

结合例子:

例 1.

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'” 是否可以看出写端倪呢?

例 2.

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

后续代码就不说了...

小结

  1. 其实 let 和 const 也是存在变量提高的,只不过和 var 的提高规则不一样,let 是将变量提高到了一个叫作 “暂存死区” 的地方,在提高时并无对其进行初始化,若是去访问 “暂存死区” 中的变量,就会报错;
  2. 因为 let 的变量提高是在 “预编译” 阶段完成的,因此在进入执行环境后,不管声明代码在何处,这个被声明的变量就被绑定了,在声明以前访问这个变量,就会报错,也就会造成 代码的 “暂时性死区”
  3. 在执行环境的执行阶段,在当执行到 let 声明时,那个变量才会 从 “暂存死区” 中移除,并对齐初始化为 undefined,因此 使用 let 声明的变量只能在其声明后访问。
  4. 其实在个人理解中,“暂存死区”“暂时性死区” 并非一个概念,“暂存死区” 亦可称为 “死缓区” (死亡缓存区域? 随便怎么叫了),“暂存死区” 是一个保存 let 声明的变量的地方,正是由于有这个区域的存在,才使得 let 声明的变量可以保证不能再声明前访问变量,继而也就造成了 “暂时性死区”

3. 全局环境下的 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 究竟是个什么东西。

干货总结

  • let 和 const 也存在变量提高,预编译阶段提高,因此能绑定整个做用域
  • let 和 const 将变量提高到了一个称为 “死缓区” 的地方,尝试访问 “死缓区” 内容将会报错,因此造成 “暂时性死区”
  • let 和 const 在提高变量时不会对其初始化操做
  • let 和 cosnt 声明的全局变量在 做用域链的顶端,一个叫 Script 的做用域里,根据做用域链规则,能够访问到其定义的全局变量

本文完,欢迎各位看官老爷批评

相关文章
相关标签/搜索