'use strict'
{
function fn() { } // SyntaxError: in strict mode code, functions may be declared only at top level or immediately within another function
console.log(fn)
}
// ⚠️ 而当前最新版本浏览器中,以上代码是不会抛出错误的。
只要通过 let、const、class 关键字声明的变量或类,都具有块级作用域。而且使用之前必须先声明,否则会抛出 ReferenceError,这个错误与“暂时性死区”(Temporal Dead Zone,TDZ)有关。
let foo = true
if (true) { // enter new scope, TDZ starts
// Uninitialized binding for `foo` is created
console.log(foo) // ReferenceError
let foo // TDZ ends, `foo` is initialized with `undefined`
console.log(foo) // undefined
foo = 1
console.log(foo) // 1
}
console.log(foo) // true
let and const declarations define variables that are scoped to the running execution context's LexicalEnvironment. The variables are created when their containing Environment Record is instantiated but may not be accessed in any way until the variable's LexicalBinding is evaluated.
A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer'sAssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.
以 let a = 1 为例,简单说明一下:
第一部分大致意思:通过 let/const 声明的变量,它会记录当前词法环境信息,且在 LexicalBinding 之前,不能以访问任何方式访问。LexicalBinding 是规则描述中的一个抽象操作,用 JS 代码比喻就是 let a = 1 中赋值操作。
第二部分,其实就是当执行代码 let a = 1 时,将赋值操作符右边的表达式的值 1 绑定到变量 a 上。若是 let a 这种形式的,那么将 undefined 值(是一个二进制的真实值)绑定到变量 a 上。这就是前一部分提到的 LexicalBinding 过程。
本将会从 ES5 中一些怪诞的行为出发,然后再到 ES6 中的
let
、const
是否会「提升」的讨论。前菜
先上个前菜,如下:
上述示例,相信这个谁都懂。再看下面这个示例:
我想很多人的答案都是打印出三个
ƒ foo()
,对吧。讲实话,在写下文章之前,我的答案也是这个。因为使用var
、function
关键字的声明语句会提升啊,因而有此答案...先不论答案对与错,我们看看各大浏览器的结果是什么:
从结果看,主要区别在于第一个
foo
打印的是undefined
或ƒ foo()
。可能 Safari 浏览器的结果更符合多数人的认知。为什么会有这样的结果,留个悬念,原因下面会介绍...
ES5 的提升
先明确一点:
原来,上面示例在 ES5 规范中是不合法的。但由于浏览器厂商都支持这个不合法的语法,只不过各 JS 引擎的实现细节上存在差异,因此才出现了前面的差异。
就比如
__proto__
从来就不是 ECMAScript 规范的一部分,但所有浏览器都支持,庆幸的是__proto__
在各引擎表现是一致的。之前文章《关于 Await、Promise 执行顺序差异问题》提到的情况,也是 JS 引擎实现差异所致。请在不要在块级作用域下声明函数,可用函数表达式替代。
在 ESLint 中规则 no-inner-declarations 就是专门检查这种情况的,若启用函数声明处会发出警告:
Move function declaration to program root.
在 ES5 严格模式下,对函数声明的某些行为做了限制。
SyntaxError
。而在当前版本(如 Chrome 92)是不会抛出语法错误的。ReferenceError
。原因是在严格模式下,fn
被提升至代码块的顶层,而不是全局作用域顶层。这点各浏览器表现是一致的。回到前面的示例(非严格模式下):
为什么行为那么怪,我们打个断点吧(以 Firefox 为例,由于 Chrome 那个
window
对象展开太多属性了,截图太影响篇幅了):看到没有,代码块中
foo
的变量是有提升至全局作用域顶层的,可......初始值是undefined
而不是ƒ foo()
,Chrome 是一样的。当代码往下执行到
function foo() { }
会更新window.foo
,因而结果就是undefined
、ƒ foo()
、ƒ foo()
。而 Safari 中,一开始
function foo() {}
提升至全局顶层时就是一个函数,所以与 Chrome、Firefox 结果不同。留两个示例,你们可以去看看都打印些什么,是否跟你们预期中的一致。尤其是第二个示例。
若第二个示例看不懂,请看分析。
ES6 会提升?
我们知道 ES6 中引入了块级作用域,自此 JavaScript 就拥有了全局作用域、函数作用域和块级作用域。
只要通过
let
、const
、class
关键字声明的变量或类,都具有块级作用域。而且使用之前必须先声明,否则会抛出ReferenceError
,这个错误与“暂时性死区”(Temporal Dead Zone,TDZ)有关。即在 TDZ starts 与 TDZ ends 的时间跨度,称为“暂时性死区”。这种机制也使得
typeof
变得不再安全,在此区间内引用变量会抛出ReferenceError
。关于更多 TDZ,请看:let/const 会提升吗?
其实,民间对于
let
等是否提升的问题,分为两派:let
没有提升行为let
还是有提升行为的无论提升与否,但我认为在实际编写代码中,大家对
let/const
的使用是毫无疑问的。因为大家对“使用前先声明”的认知是统一的。也相信很多人早就开始用let/const
全面代替var
了。无论
let/const
提升与否,几乎不会影响大家在项目中的使用,而且不会造成混乱,它们比var/function
的“提升”行为更容易区分。什么是提升?
关于“提升”行为是什么,我就不多说了,大家都知道。
但我想说,在 ECMAScript 规范中,尽管文档中不乏类似
hoisting
的单词,但就是没有对 “Hoisting” 一词作专门定义。但在前端社区中,
Hoisting
的说法确实很多。我想可能是因为,ECMAScript 就var
、function
声明语句将会前置到所在作用域顶层的行为或现象,使用了hoisting
、hoisted
等词去描述,然后在坊间互传时,在语言表述或认知理解上总会存在偏差,久而久之形成了Hoisting
的说法。综上:Hoisting 不是一个 ECMAScript 规范的术语,它只是描述了一种行为或现象。
坊间对提升的理解
举个例子,在我们眼里它只是一个再简单不过的声明语句而已。
那么 JS 引擎是怎么理解的呢。就这条简单的语句,大概会经历这些步骤:
为什么又重新提一遍这个过程,原因是:
根据社区上的普遍说法,
let
与var
的区别在于分配内存空间后是否默认存值,若使用let
不会默认存一个undefined
值。但我对这句话是有所保留的。原因如下:
在 ECMAScript 规范中关于 #14.3.1 Let and Const Declarations 有一段话是这么说的(原文略长,这里分成两段):
以
let a = 1
为例,简单说明一下:第一部分大致意思:通过
let/const
声明的变量,它会记录当前词法环境信息,且在 LexicalBinding 之前,不能以访问任何方式访问。LexicalBinding
是规则描述中的一个抽象操作,用 JS 代码比喻就是let a = 1
中赋值操作。第二部分,其实就是当执行代码
let a = 1
时,将赋值操作符右边的表达式的值1
绑定到变量a
上。若是let a
这种形式的,那么将undefined
值(是一个二进制的真实值)绑定到变量a
上。这就是前一部分提到的LexicalBinding
过程。这段话里,由始至终没有提及执行上下文初始化时,要不要为变量默认存一个
undefined
的值。这就是我说有所保留的原因。JS 引擎眼中的 TDZ
很多人都知道 TDZ 是什么回事。
但我还是那句话:TDZ 是 ECMAScript 规范中的一个术语吗?
按关键词通篇搜索 ECMAScript 文档可知,TDZ 并不是 ECMAScript 规范的术语。据说 TDZ 的说法最早出现在 ES Discussion 的讨论帖。因而,我认为 TDZ 跟 Hoisting 一样,它只是描述了一种行为或现象。这种现象就是:
前面提到
LexicalBinding
之前不允许访问,那么 JS 引擎总要告诉我们不能访问吧,于是就抛出一个ReferenceError
来提醒我们:小伙子你不能这么使用。从浏览器的角度看是否会提升?
先看个示例:
上面这两行代码,问谁都知道将会抛出
ReferenceError
。但我发现这个报错的 Message 是不同的:ReferenceError: Cannot access 'a' before initialization
ReferenceError: a is not defined
ReferenceError: Cannot access 'a' before initialization
自从 ES6 发布
let
、const
相关标准后,后续这块内容应该没有调整过了。从提示信息来看,我偏向认为 JS 引擎在实现时还是会存在“提升”行为的。再看另一示例:
我们分别从 Chrome、Firefox、Safari 中观察以上示例:
我在
let a = 1
前一行语句添加了断点,从结果上看不完全相同。Chrome、Safari 中a
的值是undefined
,而 FireFox 中a
为uninitialized
。但是它们都在let a = 1
之前就存在了变量a
,即使在下一行中都将会报错。也再次佐证了
let/const/class
声明语句是会产生提升行为。若提升,它被提升到哪?
请看示例
尽管会提升,它只会提升至当前代码块的顶层,所以在
if
语句外部无法访问a
变量,因而报错。总结
就本文内容,总结一下:
跟 let/const 无关,跟是否严格模式相关。
let/const 会提升吗?
let/const/class
确实是存在提升行为的。let/const/class
是否会提升,它必须在使用前进行定义。let/const/class
是会提升的”。若你简单从 ECMAScript 规范、浏览器调试结果去回答,这题就基本能拿满分。let/const/class
是不存在提升行为的。还是那句话,不必过分关注
let/const/class
等声明的变量或类是否存在“提升”现象。但如果作为基础拓展,还是有必要了解一下的...建议
在遇到一些不太理解的 ES6 语法的时候,不妨使用 Babel 转换一下,看看它们是怎么实现的,说不定会有灵光一现的感觉。
比如开头的示例 Babel 转换之后,就变成下面这样,然后结合自己的理解,想想为什么它要这么做。
这一招是我看某篇文章的时候学到的...
The end.