先问个简单的问题,下面哪段代码会报错?
- 先创建类实例,然后定义这个类:
1 | new Car('red'); // Does it work? |
- 先调用函数,再定义这个函数:
1 | greet('World'); // Does it work? |
正确答案是:第一段代码会抛出ReferenceError
异常,第二段代码可以正常运行。
如果你答错了,或者只是猜对了但不知道背后的原理,那么你应该了解下什么是Temporal Dead Zone (TDZ)
TDZ 负责管理let
, const
, 和class
语句的可用性。这就需要知道JavaScript变量是如何工作的。
什么是 Temporal Dead Zone
先看一个简单的 const
变量声明。如果先声明变量,然后访问它,那么一切正常:
1 | const white = '#FFFFFF'; |
现在试着在声明之前访问white
变量:
1 | white; // 抛出`ReferenceError`异常 |
这段代码里,在const white = '#FFFFFF'
语句出现之前,变量 white
处于Temporal Dead Zone。
在TDZ内访问white
,JavaScript会抛出异常ReferenceError: Cannot access 'white' before initialization
.
Temporal Dead Zone 语义禁止在未声明之前访问变量。它强调了这样的规则:不要在未声明前使用任何东西
TDZ影响到的语句
我们来看看哪些语句会受 TDZ 的影响。
const 变量
前面已经看到了,const
变量在声明和初始化前位于 TDZ:
1 | // 跪了! |
必须要在声明后使用const
变量:
1 | const pi = 3.14; |
2.2 let 变量
let
变量声明同样受 TDZ 影响:
1 | // Does not work! |
同样,要在声明之后才能使用:
1 | let count; |
class 语句
前面说了,不能在定义class
之前使用它:
1 | // 跪了! |
需要先定义再使用:
1 | class Car { |
constructor() 里的super()
如果一个类继承自另一个类,构造函数里的 this
在调用super()
之前位于TDZ:
1 | class MuscleCar extends Car { |
在 constructor()
内部, this
在 super()
调用之前不可用。要改成这样:
1 | class MuscleCar extends Car { |
函数默认参数
默认参数处于一个中间作用域,不同于全局作用域和函数作用域。默认参数同样会受到TDZ的限制:
1 | const a = 2; |
参数a
在表达式a = a
的右边被使用,同时未声明。这会产生引用错误。
要确保在声明和初始化后再使用默认参数,比如这样:
1 | const init = 2; |
var, function, import 语句
跟前面提到的语句相反,var
和function
并不受 TDZ 影响,因为会被作用域提升。
如果你在声明之前访问变量var
,只是得到 undefined
值:
1 | // 能运行,但不建议这么做 |
而函数不管在哪定义,都可以直接使用:
1 | // 稳了! |
有意思的是, import
的模块也能被提升:
1 | // 也很稳! |
尽管如此,还是建议在 JS 文件最顶部加载模块依赖。
TDZ 的typeof 行为表现
typeof
操作符可以用来判断当前作用域的某个变量是否已定义。
例如,变量notDefined
没有定义。对它使用typeof
操作符并不会抛错:
1 | typeof notDefined; // => 'undefined' |
但是对Temporal Dead Zone里的变量使用typeof
操作符,会有不同的表现:
1 | typeof variable; // throws `ReferenceError` |
这是因为,通过静态分析(用眼睛看)可以看出 variable
并不是未定义。
当前作用域的TDZ 行为
Temporal Dead Zone 只会影响到声明语句所在的作用域内的变量。
我们来看个例子:
1 | function doSomething(someVal) { |
这里有两个作用域:
- 函数作用域
let
变量所在的块级作用域
函数作用域内,typeof variable
直接返回undefined
。这里的let
变量语句不归TDZ管。
在块级作用域内,typeof variable
使用了未声明的变量,于是抛出异常ReferenceError: Cannot access 'variable' before initialization
。TDZ只在该作用域内起作用。.
结论
TDZ是一个重要的概念,它影响着const
、let
和 class
语句的可用性。它不允许在声明之前使用变量。
相反, var
变量沿用了旧有行为,可以在声明之前使用变量。但是应该避免这么做。
我觉得TDZ 是个好东西,它从语言规范层面指导了最佳编码实践。