關于 JS 中的變量提升 和 函數提升

像變量提升和函數提升這種偏學院派的問題在面試中出現的概率很高,在實際開發中也會影響到編程的效率。

前段時間在網上做面試題,發現自己對這方面知識的了解還是差了點,于是翻書看博客查文檔寫demo,最終有了這篇文章(偏新手向。

1.變量提升

通常JS在引擎在正式執行之前會進行一次預編譯,在這個過程中,首先將 變量聲明 和 函數聲明 提升到當前作用域的頂端,然后再進行下一步的處理。

在下面的代碼中,我們在函數聲明里定義了一個變量,只不過是在 if 判斷的語句塊中定義的:

function fn(){
    if(!foo){
        var foo = 5
    }
    console.log(foo);       // 5
}

fn();

運行代碼之后,我們發現結果是 5 ,為什么? 在進入 if 的判斷條件時,函數內并沒有聲明過 foo 這個變量,那么它是怎么通過判斷的呢?實際上,這就是JS引擎在預編譯時所作的事情,我們嘗試重現一下預編譯時的情形:

function fn(){
    var foo;

    if(!foo){
        foo = 5;
    }
    console.log(foo);
}

fn();

我們可以看到,不同的地方就在于 JS 把 變量聲明提升到了當前作用域的頂部(只是聲明,并沒有賦值),隨后來看 if 的判斷條件 !foo , 因為if判斷之前已經聲明了 foo ,但沒有賦值,所以 foo 的值為 undefined,轉換為布爾值之后再進行取反,得到true,所以進入if判斷,將foo賦值為5。

類似的還有下面的這個例子:

var foo = 3;

function fn(){
    var foo = foo || 5 ;
    console.log(foo) ;      //5
}

fn();

這個例子:同樣,我們嘗試還原到預編譯時的樣子:

var foo ;

function fn(){
    var foo;
    foo = foo || 5;
    console.log(foo);
}

foo = 3;

fn();

首先,在全局作用域中先聲明了一個變量 foo ,但沒有賦值。隨后通過函數聲明表達式聲明的函數被提升。其次,全局作用域中的foo被賦值為3 。最后,調用函數 fn 。 在 fn 內部的預編譯則是 首先聲明一個函數內部變量 foo,對 foo 進行賦值(等號右側是一個表達式)。

關鍵在于 對 foo 賦值的這條語句。 foo = foo || 5 ,上面說過,因為函數內部有foo,所以會依照就近原則取到內部的foo ,但此時它并沒有被賦值,所以是undefined。所以在進行賦值時, foo = 5

如果在同一個作用域中聲明了多個同名的變量,那么也是如此:

function fn(){
    var foo = 1;
    {
        var foo = 2;
    }
    console.log(foo);   //2
}

fn();

注意!因為在JS沒有塊級作用域,只有全局作用域和函數作用域【可以通過let聲明創建塊級作用域】,所以預編譯為:

function fn(){
    var foo;
    
    foo = 1;

    {
        foo = 2;
    }

    console.log(foo)    //2
}

fn();

2.函數提升

在實際開發中,我們可能會見到這樣的代碼:

function fn(){
    bar();      // 'this is bar'

    function bar(){
        console.log('this is bar')
    }
}

fn();

為什么函數可以在聲明之前就調用?并且和變量的聲明不同,它還能得到正確的結果?其實,JS引擎在預解析的時候將函數聲明整個提前到了當前作用域的頂部,預編譯之后的代碼如下:

function fn(){
    function bar(){
        console.log('this is bar')
    }

    bar();
}

與變量聲明相似的是,如果同一個作用域中有多個同名的函數聲明,那么后面聲明的會覆蓋前面聲明的:

function fn(){

    bar();      // 'this is the second bar'

    function bar(){
        console.log('this is the first bar')
    }

    bar();      // 'this is the second bar'

    function bar(){
        console.log('this is the second bar')
    }

}

fn();

預編譯之后為:

function fn(){
    
    function bar(){
        console.log('this is the first bar')
    }

    function bar(){
        console.log('this is the second bar')
    }

    bar();      // 'this is the second bar'


    bar();      // 'this is the second bar'

}

fn();

對于函數,除了使用上面的函數聲明,我們還會用到函數表達式。下面是函數聲明和函數表達式的對比:

// 函數聲明
function fn(){ console.log('this is function declaration') }

// 匿名函數表達式
var fn = function(){ 'this is anonymous function expression' }

// 具名函數表達式
var fn = function bar(){ 'this is named function expression' }

可以看到,匿名函數表達式就是將一個不帶名字的函數聲明賦值給一個變量(本質上還變量聲明,只是值換成了一個匿名函數)。而具名函數表達式,則是將一個帶名字的函數聲明重新賦值給一個變量,需要注意的是,這個函數名只能在此函數內部使用。我們也看到了,其實函數表達式可以通過變量訪問,所以也存在變量提升的效果

那么,當函數聲明 遇到 函數表達式, 會發生什么?

function fn(){
    bar();          //2

    var bar = function(){
        console.log(1)
    }

    bar();          //1

    function bar(){
        console.log(2);
    }

    bar();          //1
    
}

fn();

可能你會問,為什么是 2,1,1 ?我們還是先把它還原到預編譯時的狀態:

function fn(){
    var bar;
    function bar(){
        console.log(2)
    }
    bar();      // 2
    bar = function(){
        console.log(1)
    }
    bar();      // 1
    bar();      // 1
}

fn();

是不是一目了然?


我們再來看看當 變量 和 函數 重名時,會如何執行:

var foo = 3;

function hoistFunction() {

console.log(foo); // function foo() {}

foo = 5;

console.log(foo); // 5

function foo() {}

}

hoistFunction();

console.log(foo);   // 3

這段代碼非常有意思,我在第一次看的時候認為最后一個輸出的 foo 肯定是 5 ,因為hoistFunction 函數內部的變量 foo 沒有使用 var 定義,所以它會變成全局變量覆蓋之前定義過的全局變量 var foo = 3 。但是最后輸出的結果仍然是 3.

我們將它還原一下:

var foo = 3;

function hoistFunction(){

function foo(){}

console.log(foo);   //function foo(){}

foo = 5;

console.log(foo)

}

hoistFunction();

console.log(foo);

第一個輸出的結果應該沒有問題,那么第二個呢?

我們先來看另一段代碼

foo();

function foo(){

console.log(1000)

}

var foo = 1;

foo();

照樣先預編譯一下

function foo(){

console.log(1000)

}

foo();

var foo = 1;

foo();

你覺得結果是什么?思考一下

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

function foo(){

console.log(1000)

}

foo();  // 1000

var foo = 1;

foo();  // Uncaught TypeError: foo is not a function

為什么 foo 不是一個函數? 只有一種解釋,通過函數聲明創建的函數,它的名稱標識符被綁定在了當前作用域中,你可以理解為 這個函數的名稱標識符也是在當前作用域中定義的一個變量 ,這樣的話,接下來的變量聲明就會覆蓋掉這個函數的名稱標識符,所以 foo 不再是一個函數。

事實上,參考MDN的文檔:

Paste_Image.png

我們可以發現,官方對 以函數聲明方式創建的函數 的名稱標識符 的定義就是一個變量

好,現在再回來看最初的那個函數:

var foo = 3;

function hoistFunction(){

function foo(){}

console.log(foo);   //function foo(){}

foo = 5;    // 將 函數 foo 的名稱標識符 覆蓋

console.log(foo)

}

hoistFunction();

console.log(foo);      // 3

在 function foo(){} 這條函數聲明出現時,我們可以理解為它在函數內部定義了一個 '變量' ,隨后打印 foo ,那么毫無疑問,foo 就是被提升到當前作用域頂部的那個函數 。

接著 將 foo 這個變量(函數 foo 的名稱標識符)的賦值為 5 ,那么再次打印 foo 的時候,打印的就是那個被賦值為 5 的'變量'。

那么這也解釋了為什么在全局作用域中的 變量 foo 沒有受到影響。

因為在hoistFunction這個函數內部,在通過函數聲明創建 foo 的時候就聲明了一個變量 foo ,那么根據查找規則,【在函數作用域中有這個變量的話,就不再沿著作用域鏈向上至全局作用域中查找該變量】,所以外部變量不受影響。

我們來試著做幾個練習:


全局作用域中的變量和函數作用域中的變量:

function fn(){
    a = 5;
    console.log(window.a);      // undefined
    var a = 3;
    console.log(a)              // 3
};

fn();

普通的變量提升:

var name = 'Tom';
(function fn(){
    if(typeof name === 'undefined'){
        var name = 'Jack';
        console.log('Hello '+name);
    }else{
        console.log('GoodBye '+name);
    }
})()    //Hello Jack

函數同名情況提升:

fn1();                   //打印last

function fn1() {
    console.log("first");
}

fn1();                   //打印last

function fn1() {
    console.log("last");
}

變量名與函數名相同:

console.log(a);                     // function a(){}
function a() {}
var a = 20;
console.log(a);                     // 20

如有問題,歡迎交流 :

微信:iamyexiu
郵箱:cwork27017@126.com

參考

https://my.oschina.net/u/2949632/blog/793898

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions#Function構造函數vs函數聲明vs函數表達式

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容