[译文] JavaScript 之 Temporal Dead Zone

原文地址:Don’t Use JavaScript Variables Without Knowing Temporal Dead Zone

让我问你一个小问题,以下2个代码片段,你觉得哪一个会报错?

片段1:在定义类之前实例化一个类:

new Car('red');
class Car {
    constructor (color) {
        this.color = color;
    }
}

片段2:在定义方法之前调用该方法:

greet('World');
function greet(who) {
    console.log("Hello " + who);
}

正确答案是:片段1,报得错误是:ReferenceError: Car is not defined ;片段2能正确执行

如果你的答案不是片段1,或者你只是猜测而不是真的知道原因,那么你就得好好理解 Temporal Dead Zone( TDZ )

在 JavaScript 中,对于关键字 let ,const 和 class 来说,TDZ是个非常重要的东西

1. 什么是 Temporal Dead Zone?

让我们从一个最基础的变量定义开始,如果你先定义然后初始化这个变量,然后再访问这个变量,那么一切都会正常执行

const white = '#FFFFFF';
console.log(white); // 输出:#FFFFFF 

如果我们先访问这个变量,再定义的话

console.log(white); //抛异常:ReferenceError
const white = '#FFFFFF';

在表达式 const white=’#FFFFFF’ 之前的行, 就是变量 white的Temporal Dead Zone(禁制访问区域),在TDA访问 ‘white’会抛异常:ReferenceError: Cannot access ‘white’ before initialization

“Temporal Dead Zone”寓意定义变量之前,禁止访问这个变量。它给我们传递的信号是:未定义,勿访问

2. TDZ对表达式的影响

2.1 const 变量

如下例所示,const变量也受TDZ影响,在定义之前访问的话,会报错

pi; // throws `ReferenceError`
const pi = 3.14;

如果你在定义之后访问,则能正常执行

const pi = 3.14;
pi;

2.2 let 变量

let 变量受TDZ影响和cost变量一样

count; // throws `ReferenceError`
let count =10;

而在定义之后再访问,则正常

let count;
count; // => undefined
count = 10;
count; // => 10

2.3 class 类

如文章最开始的时候,在定义类之前,不能使用它

const myNissan = new Car('red'); // throws `ReferenceError`

class Car {
    constructor (color) {
        this.color = color;
    }
}

如果你想正确使用,那么记得在定义之后再访问

class Car {
    constructor (color) {
        this.color = color;
    }
}

const myNissan = new Car('red');
myNissan.color; // => 'red'

2.4 constructor()内的super()

如果你是继承了一个父类,那么在构造函数中,调用super()方法之前 ,this 就处于TDZ,请看如下代码:

class MuscleCar extends Car {
    constructor (color, power) {
        this.power = power;
        super(color);
    }
}

const myCar = new MuscleCar('blue', '300HP'); // `ReferenceError`

在构造函数内,在调用super()之前,this是不能使用的,TDZ 机制建议调用父类的构造函数来初始化实例,然后在子类的构造函数内对实例化的对象进行你想要的操作

2.5 函数参数

函数的默认参数处于一个全局作用域和函数作用域之间的 “中间作用域”( intermidiate scope ),这个中间区域的参数,依然要遵循TDZ的规则约束:

const a = 2;
function square(a = a) {
    return a * a;
}
square(); // throws `ReferenceError`

” function square(a = a) ” ,是想表达a = a 与 const a= 2 是处于不同的作用域中,所以可以变量名一致。而 a = a 这个表达式相当于如下代码:

a;
let a = ?; 

其中 a = a,可以分成两部分,第一部分是先执行右边,访问 a,拿到a的值,然后再执行左边 let a=?这个赋值操作。所以会出现定义之前访问 a 的问题。所以为了解决这个问题,可以把代码改成如下:

const init = 2;
function square(a = init) {
  return a * a;
}
square(); // => 4

3. TDZ对var,function,import 的影响

一句话,TDZ对这三个关键字定义的对象不起任何作用,因为他们会自动提升到当前作用域最前面。你在var变量定义之前访问它,不会报错,会得到undefined

value; // => undefined
var value;

同时,你也可以在方法被定义之前,直接调用该方法

greet('World'); // => 'Hello, World!'
function greet(who) {
  return `Hello, ${who}!`;
}
greet('Earth'); // => 'Hello, Earth!'

通常,我们对函数的实现是不太感兴趣的,这也是为什么有时候在函数定义之前调用它是有意义的。所以,import也能自动提升就是很有必要的

myFunction();
import { myFunction } from './myModule';

以上代码,能正常执行。但实际上,我们不建议这么做,import放在文件最开头是个好的编程习惯

4. 在TDZ获取变量类型

typeof 操作通用被我们用来判断当前作用域变量的类型,比如,一个名叫notDefined的变量,我们直接用typeof 去获取它类型的时候,不会报错:

console.log(typeof notDefined); //=> undefined

因为这个变量未定义,所以 typeof notDefined相当于undefined. 但是当你在TDZ区域使用 typeof 的时候,就会报引用错误,代码如下:

typeof variable; // throws `ReferenceError`
let variable;

所以,总体来说,在定义变量之前,任何访问该变量的操作都是不允许,会抛引用异常的

5. 不同作用域内的TDZ

TDZ只影响当前变量所在的作用域:

让我们来看一个详细例子:

function doSomething(someVal) {
  // 函数作用域
  typeof variable; // => undefined
  if (someVal) {
    // if内部块作用域
    typeof variable; // throws `ReferenceError`
    let variable;
  }
}

doSomething(true);

通过这个示例我们能很明确的知道,一个变量的TDZ只对它当前所在的作用域有效果

6. 总结

TDZ是影响 const、let 和 class 语句可用性的重要概念,它不允许在声明之前使用变量。相反,如果想在声明之前就可以使用变量,那么你可以继续使用var来兼容。不过你应该避免那样做。因为在我看来,TDZ是语言规范中良好的编码实践之一

发表评论

电子邮件地址不会被公开。 必填项已用*标注