let 声明会提升(hoist)吗?

更新,这里是最新的结论:zhuanlan.zhihu.com/p/28



昨天我上课的时候跟饥人谷的学生讲了《let 声明的五个特点》,其中一个就是「let 声明会提升到所在块的顶部」,然而今天早上有个学生就问我了:

MDN 上说 let 不会提升,为什么你说 let 会提升呢?

当时我心里一方:难道我讲错了?

于是我看了 MDN 英文版的原文,发现写的也是:

In ECMAScript 2015, let do not support Variable Hoisting, which means the declarations made using "let", do not move to the top of the execution block.

看来我真的错了?于是我继续翻看 ECMA-262.pdf,发现了两处地方支持我的论点。

首先是 13.3.1 Let and Const Declarations

let and const declarations define variables that are scoped to the running execution context's LexicalEnvironment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable's LexicalBinding is evaluated.

这说明即使是 block 最后一行的 let 声明,也会影响 block 的第一行。这就是提升(hoisting)(这句话存疑)。

以及 18.2.1.2 Runtime Semantics: EvalDeclarationInstantiation( body, varEnv, lexEnv, strict)

The environment of with statements cannot contain any lexical declaration so it doesn't need to be checked for var/let hoisting conflicts.

这句话从侧面证明了 let hoisting 的存在。

ECMAScript 都提到了 var/let hoisting,我不知道还有什么理由认为 let hoisting 不存在。

所以,我就把 MDN 的英文版和中文版给纠正过来了:(后来又被 TC39 的人改了)

在 ECMAScript 2015中, let 也会提升到语句块的顶部。但是,在这个语句块中,在变量声明之前引用这个变量会导致一个 ReferenceError的结果

希望被之前 MDN 某个版本误导的同学周知。

总结一下:

  1. let 声明会提升到块顶部
  2. 从块顶部到该变量的初始化语句,这块区域叫做 TDZ(临时死区)
  3. 如果你在 TDZ 内使用该变量,JS 就会报错
  4. 我可没说 TDZ 跟 hoisting 等价啊摔



更新:

有些同学还是认为 let 不会提升,试试理解下面的代码:

let a = 1
{
  a = 2
  let a
}

如果 let 不会提升,那么 a = 2 就会将外面的 a 由 1 变成 2 啊。

运行发现 a = 2 报错:Uncaught ReferenceError: a is not defined

这说明上面的代码近似近似近似近似近似近似地可以理解为:(注意看注释中的 TDZ)



let a = 1
{
  let a // TDZ 开始的地方就是这里
  'start a TDZ'
  a = 2 // 由于 a = 2 在 TDZ 中,所以报错
  a // TDZ 结束的地方就是这里
  'end a TDZ'
}

所以,let 提升了。但是由于 TDZ 的存在,你不能在声明之前使用这个变量。

更新2:

什么是 hoisting?

我并没有查到明确的定义,只能综合理解一下。

JS 的 var 的 hoisting 最好理解:不管你把 var a 写在函数的哪一行,都好像写在第一行一样;当前函数作用域里的所有 a 都表示你写的这个 a,这就是 hoisting。

以下是维基百科的释义:

Variables are lexically scoped at function level (not block level as in C), and this does not depend on order (forward declaration is not necessary): if a variable is declared inside a function (at any point, in any block), then inside the function, the name will resolve to that variable. This is equivalent in block scoping to variables being forward declared at the top of the function, and is referred to as hoisting.

什么是 TDZ?

这个依然没要找到明确的定义,大意是「在某个时间点之前,你不能访问某个变量,即使这个变量已经存在了」。

JS 的 let 中这个「时间点」就是 LexicalBinding。

JS 的 let 中这个「存在」的意思是「The variables are created」。

至于 TDZ 里这个变量有没有被声明,不是 TDZ 关心的。



我的结论:

  1. let 和 var 都有 hoisting
  2. let 有 TDZ,var 没有 TDZ



不过说句实在话,let 有没有 hoist 都无所谓,代码还不是那样写。对 let 先使用再声明的都是在耍流氓(面试官最喜欢刷流氓了)


感谢 @寸志 @胡子大哈 @Code Hz 的补充。本文相关概念确实都没有明确定义,大部分是语言设计者造的概念,所以语言使用者理解起来有差异也是正常的。

出现各种误解的症结也正是因为这些概念名没有明确的定义。有兴趣的话可以看看 GitHub 上的讨论:`let` hoisting? · Issue #767 · getify/You-Dont-Know-JS

我会在下一篇文章中详细说明什么是变量的生命周期,然后部分推翻我自己这篇文章的结论(打脸?)。(对啊,只记结论是没有用的)

完。

编辑于 2019-01-13

文章被以下专栏收录