摘要:以英文名詞來說明,是時間的暫時的意義,則是死區(qū),意指電波達(dá)不到的區(qū)域。所以可以翻為時間上暫時的無法達(dá)到的區(qū)域,簡稱為時間死區(qū)或暫時死區(qū)。以聲明的變量或常量,必需是經(jīng)過對聲明的賦值語句的求值后,才算初始化完成,創(chuàng)建時并不算初始化。
Temporal Dead Zone(TDZ)是ES6(ES2015)中對作用域新的專用語義。TDZ名詞并沒有明確地寫在ES6的標(biāo)準(zhǔn)文件中,一開始是出現(xiàn)在ES Discussion討論區(qū)中,是對于某些遇到在區(qū)塊作用域綁定早于聲明語句時的狀況時,所使用的專用術(shù)語。
以英文名詞來說明,Temporal是"時間的、暫時的"意義,Dead Zone則是"死區(qū)",意指"電波達(dá)不到的區(qū)域"。所以TDZ可以翻為"時間上暫時的無法達(dá)到的區(qū)域",簡稱為"時間死區(qū)"或"暫時死區(qū)"。
let/const與var在ES6的新特性中,最容易看到TDZ作用就是在let/const的使用上,let/const與var的主要不同有兩個地方:
let/const是使用區(qū)塊作用域;var是使用函數(shù)作用域
在let/const聲明之前就訪問對應(yīng)的變量與常量,會拋出ReferenceError錯誤;但在var聲明之前就訪問對應(yīng)的變量,則會得到undefined
console.log(aVar) // undefined console.log(aLet) // causes ReferenceError: aLet is not defined var aVar = 1 let aLet = 2
根據(jù)ES6標(biāo)準(zhǔn)中對于let/const聲明的章節(jié)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聲明的變量,當(dāng)它們包含的詞法環(huán)境(Lexical Environment)被實(shí)例化時會被創(chuàng)建,但只有在變量的詞法綁定(LexicalBinding)已經(jīng)被求值運(yùn)算后,才能夠被訪問。
注: 這里指的"變量"是let/const兩者,const在ES6定義中是constant variable(固定的變量)的意思。
說得更明白些,當(dāng)程序的控制流程在新的作用域(module, function或block作用域)進(jìn)行實(shí)例化時,在此作用域中的用let/const聲明的變量會先在作用域中被創(chuàng)建出來,但因此時還未進(jìn)行詞法綁定,也就是對聲明語句進(jìn)行求值運(yùn)算,所以是不能被訪問的,訪問就會拋出錯誤。所以在這運(yùn)行流程一進(jìn)入作用域創(chuàng)建變量,到變量開始可被訪問之間的一段時間,就稱之為TDZ(暫時死區(qū))。
以上面解說來看,以let/const聲明的變量,的確也是有提升(hoist)的作用。這個是很容易被誤解的地方,實(shí)際上以let/const聲明的變量也是會有提升(hoist)的作用。提升是JS語言中對于變量聲明的基本特性,只是因?yàn)門DZ的作用,并不會像使用var來聲明變量,只是會得到undefined而已,現(xiàn)在則是會直接拋出ReferenceError錯誤,而且很明顯的這是一個在運(yùn)行期間才會出現(xiàn)的錯誤。
用一個簡單的例子來說明let聲明的變量會在作用域中被提升,就像下面這樣:
let x = "outer value" (function() { // 這里會產(chǎn)生 TDZ for x console.log(x) // TDZ期間訪問,產(chǎn)生ReferenceError錯誤 let x = "inner value" // 對x的聲明語句,這里結(jié)束 TDZ for x }())
在例子中的IIFE里的函數(shù)作用域,變量x在作用域中會先被提升到函數(shù)區(qū)域中的最上面,但這時會產(chǎn)生TDZ,如果在程序流程還未運(yùn)行到x的聲明語句時,算是在TDZ作用的期間,這時候訪問x的值,就會拋出ReferenceError錯誤。
在let與const聲明的章節(jié)13.3.1接著的幾句,說明有關(guān)變量是如何進(jìn)行初始化的:
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.
這幾句比較重點(diǎn)的部份是關(guān)于初始化的過程。以let/const聲明的變量或常量,必需是經(jīng)過對聲明的賦值語句的求值后,才算初始化完成,創(chuàng)建時并不算初始化。如果以let聲明的變量沒有賦給初始值,那么就賦值給它undefined值。也就是經(jīng)過初始化的完成,才代表著TDZ期間的真正結(jié)束,這些在作用域中的被聲明的變量才能夠正常地被訪問。
下面這個例子是一個未初始化完成的結(jié)果,它一樣是在TDZ中,也是會拋出ReferenceError錯誤:
let x = x
因?yàn)橛抑?要被賦的值),它在此時是一個還未被初始化完成的變量,實(shí)際上我們就在這一個同一表達(dá)式中要初始化它。
函數(shù)的傳參預(yù)設(shè)值注: TDZ最一開始是為了const所設(shè)計的,但后來的對let的設(shè)計也是一致的,例子中都用let來說明會比較容易。
注: 在ES6標(biāo)準(zhǔn)中,對于const所聲明的識別子仍然也經(jīng)常為variable(變量),稱為constant variable(固定的變量)。以const聲明所創(chuàng)建出來的常量,在JS中只是不能再被賦(can"t re-assignment),并不是不可被改變(immutable)的,這兩種概念仍然有很大的差異。
TDZ作用在ES6中,很明確的就是與區(qū)塊作用域(block scope),以及變量/常量的要如何被初始化有關(guān)。實(shí)際上在許多ES6新特性中都有出現(xiàn)TDZ作用,而另一個常會被提及的是函數(shù)的傳參預(yù)設(shè)值中的TDZ作用。
下面的例子可以看到在傳參預(yù)設(shè)值的識別名稱,在未經(jīng)初始化(有賦到值)時,它會進(jìn)入TDZ而產(chǎn)生錯誤,而這個錯誤是只有在函數(shù)調(diào)用時,要使用到傳參預(yù)設(shè)值時才會出現(xiàn):
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的作用,實(shí)際上在ES6中到處都有類似的作用。
傳參預(yù)設(shè)值有另一個作用域的議題會被討論,就是對于傳參預(yù)設(shè)值的作用域,到底是屬于"全局作用域"還是"函數(shù)中的作用域"的議題,目前看到比較常見的說法是,它是處于"中介的作用域",夾在這兩者之間,但仍然會互相影響。中介的作用域的一個例子,是使用其他函數(shù)作為傳參的預(yù)設(shè)值,這通常會是一個callback(回調(diào)、回呼)函數(shù),一般的情況沒什么特別,但涉及作用域時互相影響的情況下會不易理解。下面這個例子來自這里:
let x = 1 function foo(a = 1, b = function(){ x = 2 }){ let x = 3 b() console.log(x) } foo() console.log(x)
這個例子中的最后結(jié)果,在函數(shù)foo中輸出的x值到底是1、2還是3?另外,在最外圍作用域的x最后會被改變嗎?
函數(shù)中的x輸出結(jié)果不可能是1,這是很明確的,因?yàn)楹瘮?shù)區(qū)塊中有另一個x的聲明與賦值let x = 3語句,這兩個都有可能被運(yùn)行產(chǎn)生作用。剩下的是傳參預(yù)設(shè)值中的那個函數(shù),是不是會變量到函數(shù)區(qū)塊中的x值的問題。另一個是,在全局中的那個x變量,會不會被改變,這也是一個問題。
按照這個例子的出處文檔的說明,作者認(rèn)為答案是3與1。但是根據(jù)我的實(shí)驗(yàn),下面的幾個瀏覽器與編譯器并不是這樣認(rèn)為:
babel編譯器: 2與1
Closure Compiler: 3與2
Google Chrome(v55): 3與2
Firefox(v50): 2與1
Edge(v38): 3與2
實(shí)際測試的結(jié)果,怎么都不會有3與1的答案,要不就3與2,要不就2與1。
3與2的答案是讓b傳參的x = 2運(yùn)行出來,但因?yàn)槭艿街薪樽饔糜虻挠绊懀虼烁蓴_不到函數(shù)中的原本區(qū)塊中的作用域,但會影響到全局中的x變量。也就是基本上認(rèn)定函數(shù)預(yù)設(shè)值中的那個callback中的作用域與全局(或外層)有關(guān)系。
2與1的答案則是倒過來,只會影響到函數(shù)中的區(qū)塊,對全局(或外層)沒有影響。
所以除非中介作用域,有自己獨(dú)立的作用域,完全與函數(shù)區(qū)塊中的作用域與全局都不相干,才有可能產(chǎn)生3與1的結(jié)果,這是這篇文檔的作者所認(rèn)為的。
這個函數(shù)預(yù)設(shè)值的作用域因?yàn)閷?shí)作不同,造成兩種不同的結(jié)果,但如果以Chrome(v55)與Firefox(v50)來實(shí)驗(yàn),在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()
不管如何,這個作用域的影響仍然是有爭議的,目前并沒有統(tǒng)一的答案。這代表ES6雖然標(biāo)準(zhǔn)定好了,但里面的一些新特性仍然有實(shí)作細(xì)節(jié)的差異,未來有可能這些差異才會慢慢一致。但對一般的開發(fā)者來說,因?yàn)橹懒擞羞@些情況,所以要盡量避免,以免產(chǎn)生不兼容的情況。
要如何避免這種情況?最重要的就是,"不要在傳參預(yù)設(shè)值中作有副作用的運(yùn)算",上面的function(){ x = 2 }是有副作用的,它有可能會改變函數(shù)區(qū)塊中,或是全局中的同名稱變量,而在整個代碼中,可能會互相影響的作用域彼此間,避免使用同樣識別名稱的變量,這也是一個很基本的撰寫規(guī)則。
TDZ的其它議題(陷阱) typeof語句注: 本節(jié)的內(nèi)容可以參考這幾篇文檔TEMPORAL DEAD ZONE (TDZ) DEMYSTIFIED、ES6 Notes: Default values of parameters與這個Default parameters intermediate scope討論文。
對TDZ期間中的變量/常量作任何的訪問動作,一律會拋出錯誤,使用typeof的語句也一樣。如下面的例子:
typeof x // "undefined" { // TDZ typeof x // ReferenceError let x = 42 }
但有些開發(fā)者會認(rèn)為像typeof這樣的語句,需要被用來判斷變量是否存在,不應(yīng)該是導(dǎo)致拋出錯誤,所以有部份反對的聲音,認(rèn)為它讓typeof語句變得不安全,會造成使用上的陷阱。實(shí)際上這原本就是TDZ的設(shè)計,變量本來就不該在沒聲明完成前訪問,這是為了讓JS運(yùn)行更為合理的改善設(shè)計,只是之前JS在這一部份是有缺陷的作法,實(shí)際上會用typeof與undefined來判別變量/常量存在與否的方式,通常是對于全局變量的才會作的事情。
TDZ期間拋出的錯誤是運(yùn)行階段的錯誤TDZ期間所拋出的錯誤,是一種運(yùn)行階段的錯誤,因?yàn)門DZ除了作用域的綁定過程外,還需要有變量/常量初始化的過程,才會創(chuàng)建出TDZ的期間。下面兩個例子就可以看到TDZ的錯誤需要真正運(yùn)行到才會出現(xiàn):
// 這個例子會有因TDZ拋出的錯誤 function f() { return x } f() // ReferenceError
// 這個例子不會有錯誤 function f() { return x } let x = 1
那這會有什么問題出現(xiàn)?因?yàn)橐軅蓽y出代碼中的因TDZ造成的錯誤,唯有透過靜態(tài)的代碼分析工具,或是要真正調(diào)用到函數(shù)運(yùn)行里面的代碼,才會產(chǎn)生錯誤,這將會讓TDZ在編譯工具中實(shí)作變得困難。
不過只要你理解TDZ的設(shè)計,就知道只能這樣設(shè)計,初始化過程原本就只會在調(diào)用運(yùn)行階段作這事,這部份還是只能靠其它工具來補(bǔ)強(qiáng)。
支持ES6的瀏覽器上的運(yùn)行效能在ES Discussion上對于let/const的效能很早以前就已經(jīng)有些批評的,認(rèn)為在瀏覽器上實(shí)作的結(jié)果,由于TDZ的設(shè)計,會讓let相較于var的效能至少要慢5%。
上面這篇貼文是在4年前所發(fā)表,就算是當(dāng)時的實(shí)驗(yàn)性質(zhì)的實(shí)作在JS引擎上,沒有經(jīng)過優(yōu)化,實(shí)際上真的效能有差這么大也不得而知。加上let本身在for回圈上有另外的花費(fèi),與var的設(shè)計不同,這兩個比較當(dāng)然會有所不同,是不是都是TDZ影響的也不知道。
以最近在討論區(qū)中的let與var的效能比較議題來看,let的運(yùn)行效率只有在某些情況下(for回圈中)會慢var很多,在基本的內(nèi)部作用域測試反而是快過var的,當(dāng)然這也是要視不同的瀏覽器與版本而定。
題外話是,在其它的回答中就有明確的指出,會促使加入TDZ的主因是針對const,而不是let。但最后TC39的決議是讓let與const都有一致的TDZ設(shè)計。
ES6到ES5的編譯ES6中的許多新式的設(shè)計仍然是很新的JS語言特性,目前ES6仍然需要依賴如babel之類的編譯器,將ES6語法編譯到ES5,來進(jìn)行在瀏覽器上運(yùn)行前的最后編譯。
這些編譯器對于TDZ是會如何編譯?答案是目前"并不會直接編譯"。
以babel來說,它預(yù)設(shè)不會編譯出具有TDZ的代碼,它需要額外使用babel-plugin-transform-es2015-block-scoping或編譯時的選項(xiàng)es6.blockScopingTDZ,才會將TDZ與區(qū)域作用域的功能編譯出來。基本上這應(yīng)該屬于實(shí)驗(yàn)性質(zhì)的,而且現(xiàn)在在使用上還有滿多問題的。ES5標(biāo)準(zhǔn)中原本就沒這種設(shè)計,所以說實(shí)在硬要使用也是麻煩,TDZ會造成的錯誤是運(yùn)行期間的錯誤,對于編譯器來說,在實(shí)作上也有一定的難度。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/81342.html
摘要:會出現(xiàn)這樣的情況是因?yàn)閾碛袝簳r性死區(qū)。規(guī)定暫時性死區(qū)和語句不出現(xiàn)變量提升,主要是為了減少運(yùn)行時錯誤,防止在變量聲明前就使用這個變量,從而導(dǎo)致意料之外的行為。 首先我們應(yīng)該知道js引擎在讀取js代碼時會進(jìn)行兩個步驟: 第一個步驟是解釋。 第二個步驟是執(zhí)行。 所謂解釋就是會先通篇掃描所有的Js代碼,然后把所有聲明提升到頂端,第二步是執(zhí)行,執(zhí)行就是操作一類的。 我們先來看個簡單的變量提升...
摘要:聲明一個只讀的常量。的作用域與命令相同只在聲明所在的塊級作用域內(nèi)有效。這在語法上,稱為暫時性死區(qū),簡稱。暫時性死區(qū)也意味著不再是一個百分之百安全的操作。重復(fù)聲明是允許在相同作用域內(nèi)重復(fù)聲明同一個變量的,而與不允許這一現(xiàn)象。 轉(zhuǎn)載自阮一峰老師的ES6入門,稍有修改 1.基本概念MDN var聲明了一個變量,并且可以同時初始化該變量。let語句聲明一個塊級作用域的本地變量,并且可選的賦予...
摘要:想閱讀更多優(yōu)質(zhì)文章請猛戳博客一年百來篇優(yōu)質(zhì)文章等著你引用規(guī)范作者一條最近的推特變量提升是一個陳舊且令人困惑的術(shù)語。變量提升部分提前激活是在和之前聲明變量的一種較老的方法。 為了保證可讀性,本文采用意譯而非直譯。 想閱讀更多優(yōu)質(zhì)文章請猛戳GitHub博客,一年百來篇優(yōu)質(zhì)文章等著你! 引用 ES6 規(guī)范作者 Allen Wirfs-Brock一條最近的推特: 變量提升是一個陳舊且令人困惑的...
摘要:和都能夠聲明塊級作用域,用法和是類似的,的特點(diǎn)是不會變量提升,而是被鎖在當(dāng)前塊中。聲明常量,一旦聲明,不可更改,而且常量必須初始化賦值。臨時死區(qū)臨時死區(qū)的意思是在當(dāng)前作用域的塊內(nèi),在聲明變量前的區(qū)域叫做臨時死區(qū)。 主要知識點(diǎn)有:var變量提升、let聲明、const聲明、let和const的比較、塊級綁定的應(yīng)用場景showImg(https://segmentfault.com/img...
閱讀 2444·2021-11-19 09:59
閱讀 1972·2019-08-30 15:55
閱讀 930·2019-08-29 13:30
閱讀 1330·2019-08-26 10:18
閱讀 3081·2019-08-23 18:36
閱讀 2382·2019-08-23 18:25
閱讀 1156·2019-08-23 18:07
閱讀 430·2019-08-23 17:15