提升----你所不知道的JavaScript系列(3)
很多程式語言在執行的時候都是自上而下執行,但實際上這種想法在JavaScript中並不完全正確, 有一種特殊情況會導致這個假設是錯誤的。來看看下面的程式碼,
a = 2;var a; console.log( a );
console.log(a) 會輸出什麼呢?
有些人可能會認為是 undefined,因為 var a 宣告在 a = 2 之後,他們自然而然地認為變數被重新賦值了,因此會被賦予預設值 undefined。但是,真正的輸出結果是 2。
先不急為什麼,我們再繼續看另外一段程式碼,
console.log( a );var a = 2;
鑑於上一個程式碼片段所表現出來的某種非自上而下的行為特點,你可能會認為這個程式碼片段也會有同樣的行為而輸出 2。還有人可能會認為,由於變數 a 在使用前沒有先進行宣告,因此會丟擲 ReferenceError 異常。
其實不然,兩種猜測都是不對的。輸出來的會是 undefined。
提升
引擎會在解釋 JavaScript 程式碼之前首先對其進行編譯,簡單地說,任何 JavaScript 程式碼片段在執行前都要進行編譯(通常就在執行前,說通常是因為JavaScript 中存在兩個機制可以“欺騙” 詞法作用域: eval(..) 和 with)。編譯階段中的一部分工作就是找到所有的宣告,並用合適的作用域將它們關聯起來,包括變數和函式在內的所有宣告都會在任何程式碼被執行前首先被處理。這就是我們通常說的“提升”。
注:只有宣告本身會被提升, 而賦值或其他執行邏輯會留在原地。
foo();function foo() { console.log( a ); // undefined var a = 2; }
每個作用域都會進行提升操作。所以 foo(..)函式自身也會在內部對 var a 進行提升(顯然並不是提升到了整個程式的最上方)。在這裡,你或許會發現,為什麼程式碼裡面是先呼叫 foo() ,再宣告 foo() 這樣的順序,卻不會報錯。這是因為除了變數宣告會在其作用域內提升之外,函式宣告也具有相似的特效。因此這段程式碼可以暫時理解為下面的形式:
function foo() { var a; console.log( a ); // undefined a = 2; } foo();
可以看到,函式宣告會被提升在作用域的頂部。但是有一點需要和變數宣告提升做區別的是:變數提升只是提升了變數的宣告,而變數賦值並沒有被提升。但是,函式的宣告有點不一樣,函式體也會一同被提升。
所以上面的一段暫時性的程式碼實際上可以這樣理解:
var foo = { var a; console.log( a ); // undefined a = 2; } foo();
foo 函式的宣告(這個例子還包括實際函式的隱含值)被提升了,因此第一行中的呼叫可以正常執行。
然而並不是所有的函式都能提升!函式宣告會被提升,但是函式表示式卻不會被提升。
foo(); // 不是 ReferenceError, 而是 TypeError!var foo = function bar() { // ...};
上面這段程式中的變數識別符號 foo() 被提升並分配給所在作用域,因此 foo() 不會導致 ReferenceError。但是 foo 此時並沒有賦值(如果它是一個函式宣告而不是函式表示式,那麼就會賦值)。foo() 由於對 undefined 值進行函式呼叫而導致非法操作,因此丟擲 TypeError 異常。
同時也要記住,即使是具名的函式表示式,名稱識別符號在賦值之前也無法在所在作用域中使用:
foo(); // TypeErrorbar(); // ReferenceErrorvar foo = function bar() { // ...};
這個程式碼片段經過提升後,實際上會被理解為以下形式:
var foo; foo(); // TypeErrorbar(); // ReferenceErrorfoo = function() { var bar = ...self... // ...}
這裡我們說到具名函式表示式,就順便插如一點具名函式表示式的知識點。我們看看下面的例子:
function test() { var fn = function fn1() { log(fn === fn1); // true log(fn == fn1); // true } fn(); log(fn === fn1); // Uncaught ReferenceError: fn1 is not defined log(fn == fn1); // Uncaught ReferenceError: fn1 is not defined} test();
看上面這例子,是不是很疑惑?
具名函式表示式,是帶名字的函式賦值給一個變數,這個名字只在新定義的函式作用域內有效,因為規範規定了標示符不能在外圍的作用域內有效。也就是說,這個函式名只能在此函式內部使用,可以理解為這個函式名成了函式體內部的一個變數。
這裡還有一點需要注意的,函式定義了一個非標準的name屬性,透過這個屬性可以訪問到給定函式指定的名字,這個屬性的值永遠等於跟在function關鍵字後面的識別符號,匿名函式的name屬性為空,而具名的函式表示式會修改到這個屬性。
var foo = function(){ //...}; console.log(foo.name); //foovar bar = function foobar(){ //...}; console.log(bar.name); //foobar name值被修改
函式優先
函式宣告和變數宣告都會被提升。但是一個值得注意的細節(這個細節可以出現在有多個“重複” 宣告的程式碼中)是函式會首先被提升,然後才是變數。
看一下下面的程式碼:
foo(); // 1var foo;function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); };
會輸出 1 而不是 2 ! 這個程式碼片段會被引擎理解為如下形式:
function foo() { console.log( 1 ); } foo(); // 1foo = function() { console.log( 2 ); };
var foo 儘管出現在 function foo()... 的宣告之前,但它是重複的宣告(因此被忽略了),因為函式宣告會被提升到普通變數之前。儘管重複的 var 宣告會被忽略掉, 但出現在後面的函式宣告還是可以覆蓋前面的。
foo(); // 3function foo() { console.log( 1 ); }var foo = function() { console.log( 2 ); };function foo() { console.log( 3 ); }
我們來看看下面這個,
function text1() { var a = 1; function b() { a = 10; return; function a() {} } b(); console.log(a); // ?} text1();function text2() { var a = 1; function b() { a = 10; function a() {} } b(); console.log(a); // ?} text2();
想一想,這兩段程式碼輸出的結果會是什麼?
結果都是1!為啥???
這裡需要注意的是,在 function b() 中,function a() 由於存在函式提升,上述程式碼實際上的執行程式碼是這樣子的,
function text{ var a = 1; function b() { var a = function(){}; a = 10; //return; //這個return對這段程式碼沒有任何影響 } b(); console.log(a); 1 }
是不是很神奇~~~~所以在寫程式碼的時候,就要特別注意了,不要因為 JavaScript 的提升機制導致很多莫名其妙的bug出來。
最後還有一個要強調一下,由於一個普通塊內部的函式宣告通常會被提升到所在作用域的頂部,這個過程不會像下面的程式碼暗示的那樣可以被條件判斷所控制:
foo(); // "b"var a = true;if (a) { function foo() { console.log("a"); } }else { function foo() { console.log("b"); } }
function hoistVariable() { if (!foo) { var foo = 5; } console.log(foo); // 5} hoistVariable();
小結:
我們習慣將 var a = 2; 看作一個宣告,而實際上 JavaScript 引擎並不這麼認為。它將 var a和 a = 2 當作兩個單獨的宣告, 第一個是編譯階段的任務,而第二個則是執行階段的任務。這意味著無論作用域中的宣告出現在什麼地方,都將在程式碼本身被執行前首先進行處理。可以將這個過程形象地想象成所有的宣告(變數和函式)都會被“移動”到各自作用域的最頂端,這個過程被稱為提升。
宣告本身會被提升,而包括函式表示式的賦值在內的賦值操作並不會提升。
要注意避免重複宣告,特別是當普通的 var 宣告和函式宣告混合在一起的時候,否則會引起很多危險的問題!
理解變數提升和函式提升可以使我們更瞭解這門語言,更好地駕馭它,但是在開發中,我們不應該使用這些技巧,而是要規範我們的程式碼,做到可讀性和可維護性。具體的做法是:無論變數還是函式,都必須先宣告後使用。
如果對於新的專案,可以使用let替換var,會變得更可靠,可維護性更高。值得一提的是,ES6中的class宣告也存在提升,不過它和let、const一樣,被約束和限制了,其規定,如果再宣告位置之前引用,則是不合法的,會丟擲一個異常。
所以,無論是早期的程式碼,還是ES6中的程式碼,我們都需要遵循一點,先宣告,後使用。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2894/viewspace-2810077/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 閉包—-你所不知道的JavaScript系列(4)JavaScript
- 你所不知道的JavaScript 二JavaScript
- 你所不知道的JavaScript(三)JavaScript
- JavaScript中你所不知道的陣列ArrayBufferJavaScript陣列
- 【閱文筆記】提升【你不知道的JavaScript(上)】筆記JavaScript
- 關於HTTP/3背後你所不知道的HTTP
- 你所不知道的 Transformer!ORM
- 你所不知道的cssCSS
- 你所不知道的 POST
- 你所不知道的ASP.NET Core進階系列(三)ASP.NET
- 關於JavaScript物件,你所不知道的事(一)- 先談物件JavaScript物件
- 你所不知道的XML安全XML
- 關於JavaScript物件,你所不知道的事(二)- 再說屬性JavaScript物件
- 你所不知道的 AI 進展AI
- JavaScript之你不知道的thisJavaScript
- 你不知道的JavaScript(二)JavaScript
- 你不知道的JavaScript(一)JavaScript
- 你不知道的JavaScript--Item6 var預解析與函式宣告提升JavaScript函式
- Python: 你所不知道的星號 * 用法Python
- Python中你所不知道的“隱藏技巧”!Python
- 你所不知道的 C# 10新特性C#
- 你不知道的JavaScript--Item3 隱式強制轉換JavaScript
- 你不知道的javascript之繼承JavaScript繼承
- 你不知道的javascript上卷小結JavaScript
- 精讀《你不知道的 javascript(上卷)》JavaScript
- 你所不知道的Java效能優化之String!Java優化
- 你所不知道的跨域資源共享(CORS)跨域CORS
- 你所不知道的阿里開源那些事兒阿里
- 你所不知道的 Chrome 控制檯除錯技巧Chrome除錯
- 你所不知道的Python | 字串連線的祕密Python字串
- 你所不知道的js的小知識點(1)JS
- 你不知道的javascript上卷總結(2)JavaScript
- 精讀《你不知道的javascript》中卷JavaScript
- 效能測試工具Jmeter你所不知道的內幕JMeter
- 效能測試工具LoadRunner你所不知道的內幕
- 你所不知道的 Typescript 與 Redux 型別優化TypeScriptRedux型別優化
- 你所不知道的 CSS 陰影技巧與細節CSS
- 五個你所不知道的Flutter開發細節Flutter