Temporal Dead Zone(TDZ)是ES6(ES2015)中对作用域新的专用语义。TDZ名词并没有明确地写在ES6的标准文件中,一开始是出现在ES Discussion讨论区中,是对于某些遇到在区块作用域绑定早于声明语句时的状况时,所使用的专用术语。

以英文名词来说明,Temporal是"时间的、暂时的"意义,Dead Zone则是"死区",意指"电波达不到的区域"。所以TDZ可以翻为"时间上暂时的无法达到的区域",简称为"时间死区"或"暂时死区"。

let/const与var

在ES6的新特性中,最容易看到TDZ作用就是在let/const的使用上,let/const与var的主要不同有两个地方:

  • let/const是使用区块作用域;var是使用函数作用域

  • 在let/const声明之前就访问对应的变量与常量,会抛出ReferenceError错误;但在var声明之前就访问对应的变量,则会得到undefined

console.log(aVar) // undefined
console.log(aLet) // causes ReferenceError: aLet is not defined
var aVar = 1
let aLet = 2

根据ES6标准中对于let/const声明的章节13.3.1,有以下的文字说明:

The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated.

意思是说由let/const声明的变量,当它们包含的词法环境(Lexical Environment)被实例化时会被创建,但只有在变量的词法绑定(LexicalBinding)已经被求值运算后,才能够被访问。

注: 这里指的"变量"是let/const两者,const在ES6定义中是constant variable(固定的变量)的意思。

说得更明白些,当程序的控制流程在新的作用域(module, function或block作用域)进行实例化时,在此作用域中的用let/const声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定,也就是对声明语句进行求值运算,所以是不能被访问的,访问就会抛出错误。所以在这运行流程一进入作用域创建变量,到变量开始可被访问之间的一段时间,就称之为TDZ(暂时死区)。

以上面解说来看,以let/const声明的变量,的确也是有提升(hoist)的作用。这个是很容易被误解的地方,实际上以let/const声明的变量也是会有提升(hoist)的作用。提升是JS语言中对于变量声明的基本特性,只是因为TDZ的作用,并不会像使用var来声明变量,只是会得到undefined而已,现在则是会直接抛出ReferenceError错误,而且很明显的这是一个在运行期间才会出现的错误。

用一个简单的例子来说明let声明的变量会在作用域中被提升,就像下面这样:

let x = 'outer value'(function() {// 这里会产生 TDZ for xconsole.log(x) // TDZ期间访问,产生ReferenceError错误let x = 'inner value' // 对x的声明语句,这里结束 TDZ for x
}())

在例子中的IIFE里的函数作用域,变量x在作用域中会先被提升到函数区域中的最上面,但这时会产生TDZ,如果在程序流程还未运行到x的声明语句时,算是在TDZ作用的期间,这时候访问x的值,就会抛出ReferenceError错误。

在let与const声明的章节13.3.1接着的几句,说明有关变量是如何进行初始化的:

A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression 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/const声明的变量或常量,必需是经过对声明的赋值语句的求值后,才算初始化完成,创建时并不算初始化。如果以let声明的变量没有赋给初始值,那么就赋值给它undefined值。也就是经过初始化的完成,才代表着TDZ期间的真正结束,这些在作用域中的被声明的变量才能够正常地被访问。

下面这个例子是一个未初始化完成的结果,它一样是在TDZ中,也是会抛出ReferenceError错误:

let x = x

因为右值(要被赋的值),它在此时是一个还未被初始化完成的变量,实际上我们就在这一个同一表达式中要初始化它。

注: TDZ最一开始是为了const所设计的,但后来的对let的设计也是一致的,例子中都用let来说明会比较容易。

注: 在ES6标准中,对于const所声明的识别子仍然也经常为variable(变量),称为constant variable(固定的变量)。以const声明所创建出来的常量,在JS中只是不能再被赋(can't re-assignment),并不是不可被改变(immutable)的,这两种概念仍然有很大的差异。

函数的传参预设值

TDZ作用在ES6中,很明确的就是与区块作用域(block scope),以及变量/常量的要如何被初始化有关。实际上在许多ES6新特性中都有出现TDZ作用,而另一个常会被提及的是函数的传参预设值中的TDZ作用。

下面的例子可以看到在传参预设值的识别名称,在未经初始化(有赋到值)时,它会进入TDZ而产生错误,而这个错误是只有在函数调用时,要使用到传参预设值时才会出现:

function foo(x = y, y = 1) {console.log(y)
}foo(1) // 这不会有错误
foo(undefined, 1) // 错误 ReferenceError: y is not defined
foo() // 错误 ReferenceError: y is not defined

从这个例子可以知道TDZ的作用,实际上在ES6中到处都有类似的作用。

传参预设值有另一个作用域的议题会被讨论,就是对于传参预设值的作用域,到底是属于"全局作用域"还是"函数中的作用域"的议题,目前看到比较常见的说法是,它是处于"中介的作用域",夹在这两者之间,但仍然会互相影响。中介的作用域的一个例子,是使用其他函数作为传参的预设值,这通常会是一个callback(回调、回呼)函数,一般的情况没什么特别,但涉及作用域时互相影响的情况下会不易理解。下面这个例子来自这里:

let x = 1function foo(a = 1, b = function(){ x = 2 }){let x = 3b()console.log(x)
}foo()console.log(x)

这个例子中的最后结果,在函数foo中输出的x值到底是1、2还是3?另外,在最外围作用域的x最后会被改变吗?

函数中的x输出结果不可能是1,这是很明确的,因为函数区块中有另一个x的声明与赋值let x = 3语句,这两个都有可能被运行产生作用。剩下的是传参预设值中的那个函数,是不是会变量到函数区块中的x值的问题。另一个是,在全局中的那个x变量,会不会被改变,这也是一个问题。

按照这个例子的出处文档的说明,作者认为答案是3与1。但是根据我的实验,下面的几个浏览器与编译器并不是这样认为:

  • babel编译器: 2与1

  • Closure Compiler: 3与2

  • Google Chrome(v55): 3与2

  • Firefox(v50): 2与1

  • Edge(v38): 3与2

实际测试的结果,怎么都不会有3与1的答案,要不就3与2,要不就2与1。

3与2的答案是让b传参的x = 2运行出来,但因为受到中介作用域的影响,因此干扰不到函数中的原本区块中的作用域,但会影响到全局中的x变量。也就是基本上认定函数预设值中的那个callback中的作用域与全局(或外层)有关系。

2与1的答案则是倒过来,只会影响到函数中的区块,对全局(或外层)没有影响。

所以除非中介作用域,有自己独立的作用域,完全与函数区块中的作用域与全局都不相干,才有可能产生3与1的结果,这是这篇文档的作者所认为的。

这个函数预设值的作用域因为实作不同,造成两种不同的结果,但如果以Chrome(v55)与Firefox(v50)来实验,在TDZ期间的抛出错误的行为基本上会一致,但Firefox有两种不同的错误消息,例如下面的几个例子:

// Chrome: ReferenceError: x is not defined
// Firefox: ReferenceError: x is not defined
function foo(a = 1, b = function(){ let x = 2 }){b()console.log(x)
}
foo()
// Chrome: ReferenceError: x is not defined
// Firefox: ReferenceError: can't access lexical declaration `x' before initialization
function foo(a = 1, b = function(){ x = 2 }){b()console.log(x)
}
foo()
let x = 1
// Chrome: ReferenceError: x is not defined
// Firefox: ReferenceError: can't access lexical declaration `x' before initialization
function foo(a = 1, b = function(){ x = 2 }){b()console.log(x)let x = 3
}
foo()

不管如何,这个作用域的影响仍然是有争议的,目前并没有统一的答案。这代表ES6虽然标准定好了,但里面的一些新特性仍然有实作细节的差异,未来有可能这些差异才会慢慢一致。但对一般的开发者来说,因为知道了有这些情况,所以要尽量避免,以免产生不兼容的情况。

要如何避免这种情况?最重要的就是,"不要在传参预设值中作有副作用的运算",上面的function(){ x = 2 }是有副作用的,它有可能会改变函数区块中,或是全局中的同名称变量,而在整个代码中,可能会互相影响的作用域彼此间,避免使用同样识别名称的变量,这也是一个很基本的撰写规则。

注: 本节的内容可以参考这几篇文档TEMPORAL DEAD ZONE (TDZ) DEMYSTIFIED、ES6 Notes: Default values of parameters与这个Default parameters intermediate scope讨论文。

TDZ的其它议题(陷阱)

typeof语句

对TDZ期间中的变量/常量作任何的访问动作,一律会抛出错误,使用typeof的语句也一样。如下面的例子:

typeof x // "undefined"{// TDZtypeof x // ReferenceErrorlet x = 42
}

但有些开发者会认为像typeof这样的语句,需要被用来判断变量是否存在,不应该是导致抛出错误,所以有部份反对的声音,认为它让typeof语句变得不安全,会造成使用上的陷阱。实际上这原本就是TDZ的设计,变量本来就不该在没声明完成前访问,这是为了让JS运行更为合理的改善设计,只是之前JS在这一部份是有缺陷的作法,实际上会用typeof与undefined来判别变量/常量存在与否的方式,通常是对于全局变量的才会作的事情。

TDZ期间抛出的错误是运行阶段的错误

TDZ期间所抛出的错误,是一种运行阶段的错误,因为TDZ除了作用域的绑定过程外,还需要有变量/常量初始化的过程,才会创建出TDZ的期间。下面两个例子就可以看到TDZ的错误需要真正运行到才会出现:

// 这个例子会有因TDZ抛出的错误
function f() { return x }
f() // ReferenceError
// 这个例子不会有错误
function f() { return x }
let x = 1

那这会有什么问题出现?因为要能侦测出代码中的因TDZ造成的错误,唯有透过静态的代码分析工具,或是要真正调用到函数运行里面的代码,才会产生错误,这将会让TDZ在编译工具中实作变得困难。

不过只要你理解TDZ的设计,就知道只能这样设计,初始化过程原本就只会在调用运行阶段作这事,这部份还是只能靠其它工具来补强。

支持ES6的浏览器上的运行效能

在ES Discussion上对于let/const的效能很早以前就已经有些批评的,认为在浏览器上实作的结果,由于TDZ的设计,会让let相较于var的效能至少要慢5%。

上面这篇贴文是在4年前所发表,就算是当时的实验性质的实作在JS引擎上,没有经过优化,实际上真的效能有差这么大也不得而知。加上let本身在for回圈上有另外的花费,与var的设计不同,这两个比较当然会有所不同,是不是都是TDZ影响的也不知道。

以最近在讨论区中的let与var的效能比较议题来看,let的运行效率只有在某些情况下(for回圈中)会慢var很多,在基本的内部作用域测试反而是快过var的,当然这也是要视不同的浏览器与版本而定。

题外话是,在其它的回答中就有明确的指出,会促使加入TDZ的主因是针对const,而不是let。但最后TC39的决议是让let与const都有一致的TDZ设计。

ES6到ES5的编译

ES6中的许多新式的设计仍然是很新的JS语言特性,目前ES6仍然需要依赖如babel之类的编译器,将ES6语法编译到ES5,来进行在浏览器上运行前的最后编译。

这些编译器对于TDZ是会如何编译?答案是目前"并不会直接编译"。

以babel来说,它预设不会编译出具有TDZ的代码,它需要额外使用babel-plugin-transform-es2015-block-scoping或编译时的选项es6.blockScopingTDZ,才会将TDZ与区域作用域的功能编译出来。基本上这应该属于实验性质的,而且现在在使用上还有满多问题的。ES5标准中原本就没这种设计,所以说实在硬要使用也是麻烦,TDZ会造成的错误是运行期间的错误,对于编译器来说,在实作上也有一定的难度。

理解ES6中的暂时死区(TDZ)相关推荐

  1. 理解es6中的暂时性死区

    引入 什么是作用域? 一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域. 全局作用域 JS中没有明确的全局作用域的概念,只有局部作用域以及全局执行环 ...

  2. 理解ES6中的TDZ(暂时性死区)

    什么是TDZ Temporal Dead Zone(TDZ)是ES6(ES2015)中对作用域新的专用语义.TDZ名词并没有明确地写在ES6的标准文件中,一开始是出现在ES Discussion讨论区 ...

  3. 理解ES6中暂时性死区TDZ

    什么是暂时性死区 ES6中,在代码块内,使用let/const命令声明变量之前,该变量都是不可用的,在变量声明之前属于该变量的"死区".这在语法上,称为"暂时性死区&qu ...

  4. 这篇看完我得理解ES6中中常见语法

    目录 前言 1let篇 1.1作用域 1.2变量提升 1.3相同作用域赋值 2const篇 3模板字符串篇 3.1传统 3.2模板字符串复制 4扩展运算符篇 4.1传统赋值 4.2扩展字符串复制 前言 ...

  5. 撤底理解es6中的箭头函数

    本质上 是一个函数,是function 是一个被编译层加工过的函数 用 babel 编译一下箭头函数看看,如下 //es6 const a = ()=>{ console.log(this) } ...

  6. 深入解析ES6中let和闭包

    2019独角兽企业重金招聘Python工程师标准>>> 本篇文章主要介绍了深入理解ES6中let和闭包,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下.如有不 ...

  7. 理解JavaScriptES6中的TDZ(暂时性死区)

    什么是TDZ Temporal Dead Zone(TDZ)是ES6中对作用域新的专用语义.TDZ名词并没有明确地写在ES6的标准文件中,一开始是出现在ES Discussion讨论区中,是对于某些遇 ...

  8. ES2015中let的暂时性死区(TDZ)

    Tomporal Dead Zone (TDZ)是ES2015中对作用域新的专用定义.是对于某些遇到在区块作用域绑定早于声明语句时的情况.Tomporal Dead Zone (TDZ)可以理解为时间 ...

  9. [js] 说说你对JS中暂性死区的理解,它有什么运用场景?

    [js] 说说你对JS中暂性死区的理解,它有什么运用场景? var v=1; {v = 2;let v; }这里候会报错: Cannot access 'v' before initializatio ...

最新文章

  1. StaticFactoryMethod_Level2
  2. TCODE找相應的BAPI(主要是找到包PACKAGE)
  3. 简单tarjan》一道裸题(BZOJ1051)(easy)
  4. kafka集群管理工具kafka-manager
  5. java基础---System类
  6. linux常用特殊符号
  7. 上银伺服驱动器说明书_威海伺服驱动器维修,诚信互利
  8. 谷歌浏览器chrome安装Hackbar插件方式
  9. Linux定时器函数setitimer
  10. HDU 1063 [Exponentiation]高精度
  11. Python读取nc文件
  12. sap和erp的区别:
  13. 利用MeGUI实现批量转换视频
  14. 【原创开源应用第2期】基于RL-USB和RL-FlashFS的完整NAND解决方案,稳定好用,可放心用于产品批量
  15. 51单片机电机测速程序c语言,基于51单片机光电编码器测速.doc
  16. English Pod 听力学习之路 C41 - C68
  17. 机器视觉 飞拍曝光计算
  18. 常用数学公式 关于正态分布的20210806
  19. 论微服务架构设计与应用
  20. Padavan 路由器获取唯一IPv6 解决无网络访问权限

热门文章

  1. RISC-V架构能否引领物联网时代?
  2. PS基础教程[6]如何快速制作一寸照片
  3. VUE+Nodejs 商城项目练习项目(前台购物)
  4. android 添加子view,如何在Android中向ImageView添加子视图
  5. 【图像处理】记一次粗心:未加载opencv_world300d.dll
  6. AR涂涂乐⭐二、 给material赋予材质、移除material、调整扫描框透明度
  7. 禁止网页 切屏 切换标签
  8. python如何在图片上添加文字_Python在图片中添加文字的两种方法
  9. 自己制作dns解析服务器,如何使用自己的DNS服务器解析域名,架设自己的DNS服务器...
  10. SAP SD 销售中的借贷项凭证