Temporal Dead Zone(TDZ)是ES6(ES2015)中對做用域新的專用語義。TDZ名詞並無明確地寫在ES6的標準文件中,一開始是出如今ES Discussion討論區中,是對於某些遇到在區塊做用域綁定早於聲明語句時的情況時,所使用的專用術語。git
以英文名詞來講明,Temporal是"時間的、暫時的"意義,Dead Zone則是"死區",意指"電波達不到的區域"。因此TDZ能夠翻爲"時間上暫時的沒法達到的區域",簡稱爲"時間死區"或"暫時死區"。es6
在ES6的新特性中,最容易看到TDZ做用就是在let/const的使用上,let/const與var的主要不一樣有兩個地方:github
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,有如下的文字說明:babel
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)已經被求值運算後,纔可以被訪問。ecmascript
注: 這裏指的"變量"是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 x console.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 = 1 function foo(a = 1, b = function(){ x = 2 }){ let x = 3 b() console.log(x) } foo() console.log(x)
這個例子中的最後結果,在函數foo中輸出的x值究竟是一、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
的語句也同樣。以下面的例子:
typeof x // "undefined" { // TDZ typeof x // ReferenceError let x = 42 }
但有些開發者會認爲像typeof
這樣的語句,須要被用來判斷變量是否存在,不該該是致使拋出錯誤,因此有部份反對的聲音,認爲它讓typeof
語句變得不安全,會形成使用上的陷阱。實際上這本來就是TDZ的設計,變量原本就不應在沒聲明完成前訪問,這是爲了讓JS運行更爲合理的改善設計,只是以前JS在這一部份是有缺陷的做法,實際上會用typeof
與undefined來判別變量/常量存在與否的方式,一般是對於全局變量的纔會做的事情。
TDZ期間所拋出的錯誤,是一種運行階段的錯誤,由於TDZ除了做用域的綁定過程外,還須要有變量/常量初始化的過程,纔會建立出TDZ的期間。下面兩個例子就能夠看到TDZ的錯誤須要真正運行到纔會出現:
// 這個例子會有因TDZ拋出的錯誤 function f() { return x } f() // ReferenceError
// 這個例子不會有錯誤 function f() { return x } let x = 1
那這會有什麼問題出現?由於要能偵測出代碼中的因TDZ形成的錯誤,惟有透過靜態的代碼分析工具,或是要真正調用到函數運行裏面的代碼,纔會產生錯誤,這將會讓TDZ在編譯工具中實做變得困難。
不過只要你理解TDZ的設計,就知道只能這樣設計,初始化過程本來就只會在調用運行階段做這事,這部份仍是隻能靠其它工具來補強。
在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中的許多新式的設計仍然是很新的JS語言特性,目前ES6仍然須要依賴如babel之類的編譯器,將ES6語法編譯到ES5,來進行在瀏覽器上運行前的最後編譯。
這些編譯器對於TDZ是會如何編譯?答案是目前"並不會直接編譯"。
以babel來講,它預設不會編譯出具備TDZ的代碼,它須要額外使用babel-plugin-transform-es2015-block-scoping或編譯時的選項es6.blockScopingTDZ
,纔會將TDZ與區域做用域的功能編譯出來。基本上這應該屬於實驗性質的,並且如今在使用上還有滿多問題的。ES5標準中本來就沒這種設計,因此說實在硬要使用也是麻煩,TDZ會形成的錯誤是運行期間的錯誤,對於編譯器來講,在實做上也有必定的難度。