谈谈 JavaScript 中的 TDZ

Posted on 2017-02-16 in Javascript by yucongchen

本来过年期间想写这个的,不过要准备些东西,一直没抽出时间,刚好今天有点空闲。上个月阮一峰阮老师在微博上发布了这样一条信息:

谈谈 JavaScript 中的 TDZ - Beta空间 - imbeta.cn

于是评论区炸开了锅,很多人留言指出,这是 TDZ。 TDZ 全名 Temporal Dead Zone,翻译过来就是暂时性死区。今天就简单谈一下,我运行代码的环境是 node 6.9.1 。

JS 中的变量提升

我们都知道,在 JS 中,使用 var 声明的变量会被提升(Hosting),也就是不管你在什么地方写的 var,都会把其提升到作用域最开头。比如:

x = 5;
console.log(x);  // 5
var x;

这段代码可以运行,除了丑陋了点,写的顺序上,你先用了变量 x,然后才声明 x。其实 JS 引擎在执行这段代码的时候,会自动把声明给提升到最前面,也就是:

var x;
x = 5;
console.log(x);  // 5

这时我们再看另一个例子:

console.log(x);  // undefined
var x = 5;

这个例子输出的是 undefined,说明提升的时候仅仅是提升了声明,并没有把初始化也提升上去,而且在用 var 声明的时候如果没有赋值,JS 引擎就是自动初始化为『undefined』。

搞懂了这个,我们再来看一个作用域有关的例子:

var a = [1,2,3];
for(var i=0; i<a.length;i+=1){
    setTimeout(function(){
        console.log(a[i]);
    }, i*1000)
}

这里我们本想隔一秒输出一个数字的,但是实际上是输出了三次 undefined,这里是因为循环结束之后,i 的值变为3,而等到 setTimeout 开始执行的时候 a[i] 就变成 a[3],输出的结果自然就是 undefined。

如果想实现上面的功能,就要用到我们的闭包,把变量 i 给留住:

var a = [1,2,3];
for(var i=0; i<a.length;i++){
    (function(b){
        setTimeout(function(){
            console.log(a[b]);
        }, i*1000)
    })(i)
}

未初始化(uninitialised)

使用 var 进行变量声明时如果没有初始化,会自动初始化为 undefined,这个特性和变量提升结合在一起,常常会造成一些难以定位的 bug,因为眼睛容易被欺骗,看起来作用域范围是合理的。

这可能是 JavaScript 设计之初的一个错误,但是为了保持向后兼容,意味着永远改变不了 JavaScript 在浏览器中的行为。当 JavaScript 的发明人 Brendan Eich 决定修复这个问题时,他添加了一个新的关键词,就是 『let』。

let 和 var 一样,也可以用来声明变量,但是有很多好处:

  • let 声明的变量是块级作用域的。也就是 let 声明的变量只是外层块,而不是整个外层函数。
  • let 声明的全局变量不是全局对象的属性。 所以,你不可以通过 window.xxx 的方式访问 let 声明的变量。
  • 在 for 循环中,每次迭代都会创建新的绑定。这句话说的比较拗口,直接看上面那个例子,我们做一点修改,
var a = [1,2,3];
for(let i=0; i<a.length;i+=1){
    setTimeout(function(){
        console.log(a[i]);
    }, i*1000)
}

我们把 for(var i=0; i<a.length;i++) 改为 for(let i=0; i<a.length;i++) 只是改了声明变量 i 的方式,在 node 命令行中可以看到是可以正常输出 1,2,3 的。

  • 用 let 重新定义一个变量时会抛出语法错误。
let a = 1;
let a = 2;

这里抛出错误: SyntaxError: Identifier 'a' has already been declared。

  • let 声明的变量知道运行到被定义的代码时才会被初始化,所以在初始化之前使用会触发错误。这条其实是这里比较重要的一个点,简单来说,let 不会自动初始化成 undefined,如果你在初始化前用了 let 声明的变量,那么不好意思,引擎不想说话,并会丢一个 ReferenceError 给你。而且 let 声明的变量也会被提升,于是乱用就等着接异常吧。看个例子:
console.log(a);
let a = 1;

运行时直接就会报:ReferenceError: a is not defined。 上面的代码就类似于:

let a;
console.log(a);
a = 1;

如果只用 let 对变量进行声明,而没有做初始化,那么变量就会是未初始化(uninitialised)状态,在这个状态下你只要用这个变量,就会接到引擎抛来的异常。而变量从仅声明未初始化状态到初始化完成之间这个等待的时间,就叫做 TDZ 暂时性死区(这铺垫,好长)。

弄清楚这些之后,我们再来看看阮老师的代码为什么会被 V8 报错。

TDZ

ES6 中默认参数机制就是类似 let 声明一样,于是,阮老师的代码可以理解为:

var x = 1;
function foo( let x = x){
    console.log(x);
}
foo()

这里,let x = x。作为默认参数值的 x 和 作为形参的 x 撞车了,默认参数值 x 需要参数 x 的值,而参数 x 又需要默认参数 x 的值,于是无法完成初始化,形成暂时性死区。这里的,作用域只在函数内,和外面用 var 声明的 x 没关系。

要想运行,把形参值换一下就好:

var x = 1;
function foo(a = x){
    console.log(a);
}
foo()

这样就可以顺利输出了。

现在来考虑以下两种情况:

function a(x = y, y) {
    console.log(x);
}
function b(x, y = x){
    console.log(y);
}
a(undefined, 1)
b(1, undefined)

你认为 a,b 两个函数哪个函数能运行成功呢?

碎碎念

记录一些所思所想,写写科技与人文,写写生活状态,写写读书感悟,欢迎关注,交流。

微信公众号:程序员的诗和远方

公众号ID : MonkeyCoder-Life

程序员的诗和远方

参考

http://stackoverflow.com/questions/31219420/are-variables-declared-with-let-or-const-not-hoisted-in-es6

http://stackoverflow.com/questions/33198849/what-is-the-temporal-dead-zone

http://www.infoq.com/cn/articles/es6-in-depth-let-and-const