摘要:以英文名詞來說明,是時間的暫時的意義,則是死區,意指電波達不到的區域。所以可以翻為時間上暫時的無法達到的區域,簡稱為時間死區或暫時死區。以聲明的變量或常量,必需是經過對聲明的賦值語句的求值后,才算初始化完成,創建時并不算初始化。
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 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值到底是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 }是有副作用的,它有可能會改變函數區塊中,或是全局中的同名稱變量,而在整個代碼中,可能會互相影響的作用域彼此間,避免使用同樣識別名稱的變量,這也是一個很基本的撰寫規則。
TDZ的其它議題(陷阱) typeof語句注: 本節的內容可以參考這幾篇文檔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的錯誤需要真正運行到才會出現:
// 這個例子會有因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會造成的錯誤是運行期間的錯誤,對于編譯器來說,在實作上也有一定的難度。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/81342.html
摘要:會出現這樣的情況是因為擁有暫時性死區。規定暫時性死區和語句不出現變量提升,主要是為了減少運行時錯誤,防止在變量聲明前就使用這個變量,從而導致意料之外的行為。 首先我們應該知道js引擎在讀取js代碼時會進行兩個步驟: 第一個步驟是解釋。 第二個步驟是執行。 所謂解釋就是會先通篇掃描所有的Js代碼,然后把所有聲明提升到頂端,第二步是執行,執行就是操作一類的。 我們先來看個簡單的變量提升...
摘要:聲明一個只讀的常量。的作用域與命令相同只在聲明所在的塊級作用域內有效。這在語法上,稱為暫時性死區,簡稱。暫時性死區也意味著不再是一個百分之百安全的操作。重復聲明是允許在相同作用域內重復聲明同一個變量的,而與不允許這一現象。 轉載自阮一峰老師的ES6入門,稍有修改 1.基本概念MDN var聲明了一個變量,并且可以同時初始化該變量。let語句聲明一個塊級作用域的本地變量,并且可選的賦予...
摘要:想閱讀更多優質文章請猛戳博客一年百來篇優質文章等著你引用規范作者一條最近的推特變量提升是一個陳舊且令人困惑的術語。變量提升部分提前激活是在和之前聲明變量的一種較老的方法。 為了保證可讀性,本文采用意譯而非直譯。 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! 引用 ES6 規范作者 Allen Wirfs-Brock一條最近的推特: 變量提升是一個陳舊且令人困惑的...
摘要:和都能夠聲明塊級作用域,用法和是類似的,的特點是不會變量提升,而是被鎖在當前塊中。聲明常量,一旦聲明,不可更改,而且常量必須初始化賦值。臨時死區臨時死區的意思是在當前作用域的塊內,在聲明變量前的區域叫做臨時死區。 主要知識點有:var變量提升、let聲明、const聲明、let和const的比較、塊級綁定的應用場景showImg(https://segmentfault.com/img...
閱讀 2474·2021-11-19 09:59
閱讀 1995·2019-08-30 15:55
閱讀 936·2019-08-29 13:30
閱讀 1339·2019-08-26 10:18
閱讀 3088·2019-08-23 18:36
閱讀 2390·2019-08-23 18:25
閱讀 1164·2019-08-23 18:07
閱讀 440·2019-08-23 17:15