提升----你所不知道的JavaScript系列(3)

wyfem發表於2021-09-09

很多程式語言在執行的時候都是自上而下執行,但實際上這種想法在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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章