不知道临时死区就不要使用JavaScript变量
作者:Dmitri Pavlutin 原文:dmitripavlutin.com/javascript-…
先问个简单的问题,下面哪段代码会报错? 第一段代码是先创建实例然后再定义用到的类:
new Car('red'); // Does it work?
class Car {
constructor(color) {
this.color = color;
}
}
或者是第二段先调用再定义函数?
greet('World'); // Does it work?
function greet(who) {
return `Hello, ${who}!`;
}
正确答案:第一段代码,在new
对象时报ReferenceError
。第二段成功执行。
如果你的答案和上面的不一样,或者你不知道底层的原理直接猜了个答案,就说明你需要了解临时死区(Temporal Dead Zone以下简写为TDZ)。
TDZ管理let
, const
和class
语句的可用性,所以了解JavaScript中变量是如何工作的很重要。
1.什么是临时死区
先从简单的const
常量声明开始,如果第一次声明和初始化变量,然后进行访问,代码会按我们所预期的那样执行:
const white = '#FFFFFF';
white; // => '#FFFFFF'
现在试着在声明前访问变量white:
white; // throws `ReferenceError`
const white = '#FFFFFF';
white;
在 const white = '#FFFFFF'
语句之前的代码行里,变量white就在临时死区里。
在TDZ里访问white
,JavaScript抛出ReferenceError:Cannot access 'white' before initialization
。

“临时死区的语义中禁止在变量声明前访问,它强调了规则:在声明前不能使用任何东西”
2.TDZ影响的语句
看一下TDZ影响的语句
2.1 const常量
前面已经看到,const常量在声明和初始化代码行前是在TDZ中的:
// Does not work!
pi; // throws `ReferenceError`
const pi = 3.14;
必须使用声明后的const
常量。
const pi = 3.14;
// Works!
pi; // => 3.14
2.2 let变量
let
声明语句同样在声明行前是受TDZ影响的:
// Does not work!
count; // throws `ReferenceError`
let count;
count = 10;
同样,只有在声明之后才能使用let
变量。
let count;
// Works!
count; // => undefined
count = 10;
// Works!
count; // => 10
2.3 class语句
就像在介绍中看到的,不能在定义前使用class
:
// Does not work!
const myNissan = new Car('red'); // throws `ReferenceError`
class Car {
constructor(color) {
this.color = color;
}
}
要使得上面代码正确,在class
定义之后再使用:
lass Car {
constructor(color) {
this.color = color;
}
}
// Works!
const myNissan = new Car('red');
myNissan.color; // => 'red'
2.4 constructor()内的super()
如果要扩展父类,在constructor
内调用super()
之前,this
绑定是在TDZ中:
class MuscleCar extends Car {
constructor(color, power) {
this.power = power;
super(color);
}
}
// Does not work!
const myCar = new MuscleCar('blue', '300HP'); // `ReferenceError`
在constructor()
内,this
在super()
调用前是不能使用的。
TDZ推荐调用父类的构造函数来初始化实例,在这之后,实例初始化完成,你就可以在子构造函数中做一些调整。
class MuscleCar extends Car {
constructor(color, power) {
super(color);
this.power = power;
}
}
// Works!
const myCar = new MuscleCar('blue', '300HP');
myCar.power; // => '300HP'
2.5默认函数参数
默认参数在中间作用域里,与全局作用域和函数作用域分离。默认参数同样受到DTZ的限制:
const a = 2;
function square(a = a) {
return a * a;
}
// Does not work!
square(); // throws `ReferenceError`
参数a
是在表达式a=a
的右边,这个时候还未声明,因此在引用a
的时候会报引用错误。
为了确保默认参数在声明和初始化之后使用,这里使用一个特殊变量init
在使用前进行初始化。
const init = 2;
function square(a = init) {
return a * a;
}
// Works!
square(); // => 4
3.var, function, import语句
与上述提到的相反,var
和函数定义不受TDZ的影响,他们会被提升到当前作用域中。
如果在声明前访问var
变量,必然得到undefined
:
// Works, but don't do this!
value; // => undefined
var value;
然而,不管函数在哪里定义,都可以使用它。
// Works!
greet('World'); // => 'Hello, World!'
function greet(who) {
return `Hello, ${who}!`;
}
// Works!
greet('Earth'); // => 'Hello, Earth!'
很多时候你不关心函数的实现,而只是想要调用它,这就是有时候要在定义前调用函数的原因了。
有意思的是import
模块也会被提升:
// Works!
myFunction();
import { myFunction } from './myModule';
当import
提升后,好的做法是在JavaScript文件头部加载模块的依赖。
4. 在TDZ中的typeof
在要判断变量在当前作用域内是否已定义时,使用typeof
操作符会很有用。
例如,变量notDefined
没有定义,在这个变量上使用typeof
不会报错:
typeof notDefined; // => 'undefined'
因为变量没有定义,typeof notDefined
就等于undefined
。
但在TDZ中typeof
操作符用在变量上就会有不同的结果,下面的例子,JavaScript会报错:
typeof variable; // throws `ReferenceError`
let variable;
引用错误背后的原因是你可以静态(直接看代码)的确定变量已经定义过了。
5. TDZ只在当前作用域中有效
临时死区只在声明语句当前所在的作用域下影响变量。

看个例子:
function doSomething(someVal) {
// Function scope
typeof variable; // => undefined
if (someVal) {
// Inner block scope
typeof variable; // throws `ReferenceError`
let variable;
}
}
doSomething(true);
代码中有两个作用域:
1.函数作用域
2.let
变量定义所在的内部块作用域
在函数作用域中,typeof variable
就等于undefined
。在这里let
变量的TDZ没用效果。
typeof variable
的内部作用域中,在声明前使用变量就抛出了ReferenceError:Cannot access 'variable' before initialization
。TDZ只存在内部作用域中。
6. 总结
TDZ是个很重要的概念,它影响了const
, let
和class
语句的能力,它使得变量在声明前不允许使用。
相反,你可以在声明前使用var
变量,它继承了老的传统,应该尽量避免使用。
我的观点是,实际情况下当好的编码涉及到语言规范时,TDZ就是一种好的选择。
转载自:https://juejin.cn/post/6844903958356459527