let 声明会提升(hoist)吗?

20

本文为饥人谷讲师方方原创文章,首发于 前端学习指南

前端日子我上课的时候跟饥人谷的学生讲了《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

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

加微信号: astak10或者长按识别下方二维码进入前端技术交流群 ,暗号:写代码啦

每日一题,每周资源推荐,精彩博客推荐,工作、笔试、面试经验交流解答,免费直播课,群友轻分享... ,数不尽的福利免费送

你可能感兴趣的

日光 · 2017年12月28日

所以let就是提升了变量也没用?

+2 回复

0

@日光 提升了,但是因为有TDZ,所以在let声明之前访问会触发ReferemceError

WickedDogg · 2017年12月28日
cyh41 · 2018年01月05日

那本你不知道的js也是写let不会在块作用域中提升

回复

simpleru · 2018年01月06日

感觉就是理解问题。
若是理解成不会提升,并且出现的区域生成块级作用于,那么声明前使用let 的变量,就会出现错误。(和其他语言就一致性了)

回复

撒网要见鱼 · 2018年01月19日

个人觉得结合VO来讲是不是会更好。本文中这样讲感觉容易引起误会。

只需要理解:在进入特定作用域时,VO就先确定变量对象

// -> 进入这个外部scope区域时,外部VO内此时有a,但是在let声明前,此变量无法使用
let a // 进入let声明,正式声明变量,此时a的默认值是undefined(声明后才能使用)
a = 1 // a赋值为1
{
// -> 进入这个内部scope区域时,内部VO内此时有a,但是在let声明前,此变量无法使用
  a = 2 // 由于此时尚未声明,所以调用a时报引用错误
  let a // 正常是到这一步声明a,并默认赋值undefined,可惜由于前面报错,所以进不来了
}

所以是不是这样简单点描述的话更易理解:

  • let a时,代表声明,a,并有一个默认值undefined
  • 如果在变量声明之前使用变量,会有引用错误(因为此时虽然VO中是有这个变量的,但是尚未声明,无法使用)

补充:

所以,可以说,实际上变量声明并未提升,仅仅是因为VO内存的是本作用域内对应的变量对象(本作用域内有,自然不会往上回溯)

不知道上述描述是否合理呢?

回复

0

补充下,防止概念被理解错误:

  1. 进入这个外部scope区域时指的是进入这个块级作用域内,可以理解为进入这个执行上下文(Execution Context)
  2. 变量调用的顺序:先在本执行上下文内的VO属性中查找对于的变量对象,如果没有查找到,则顺着scope chain往上找,直到某一个执行上下文中的VO属性中查找到或报错

所以,这应该更容易理解了,为什么块级作用域内使用了let声明,而let声明是不会提升的,但是却无法查找到外部的变量。

因为块级作用域内的VO中以及有了这个变量(文中的a),所以无法回溯了。

再强调下: 变量声明后才可使用,如果抛出引用错误,那么肯定是因为没有声明,所以let声明变量时并没有提升

虽然这句话描述起来很简单,隐藏了很多细节,但是确实是最最容易理解的。

撒网要见鱼 · 2018年01月19日
载入中...