JavaScript的作用域和提升機(jī)制

2018-06-16 20:16 更新

你知道下面的JavaScript代碼執(zhí)行時(shí)會(huì)輸出什么嗎?

var foo = 1;
function bar() {
    if (!foo) {
        var foo = 10;
    }
    alert(foo);
}
bar();

答案是“10”,吃驚嗎?那么下面的可能會(huì)真的讓你大吃一驚:

var a = 1;
function b() {
    a = 10;
    return;
    function a() {}
}
b();
alert(a);

這里瀏覽器會(huì)彈出“1”。怎么回事?這似乎看起來(lái)是奇怪,未知,讓人混淆的,但這實(shí)際上是這門(mén)語(yǔ)言一個(gè)強(qiáng)大和富有表現(xiàn)力的特性。我不知道這一特性行為是否有標(biāo)準(zhǔn)名字,但我喜歡這個(gè)術(shù)語(yǔ)“提升(hoisting)”。本文試圖揭示這一特性的機(jī)制,但首先讓我們鏈接JavaScript的作用域。

JavaScript中的作用域(scope)

JavaScript初學(xué)者最容易混淆的地方是作用域。實(shí)際上,不只是初學(xué)者。我遇到過(guò)許多經(jīng)驗(yàn)豐富的JavaScript程序員,卻不完全明白作用域。JavaScript的作用域如此容易混淆的原因是它看起來(lái)很像C家族的語(yǔ)言(類(lèi)C語(yǔ)言)??紤]下面的C程序:

#include <stdio.h>
int main() {
    int x = 1;
    printf("%d, ", x); // 1
    if (1) {
        int x = 2;
        printf("%d, ", x); // 2
    }
    printf("%d\n", x); // 1
}

程序的輸出是1,2,1.這是因?yàn)镃和C家族的語(yǔ)言有塊級(jí)作用域(block-level scope)。當(dāng)控制流進(jìn)入一個(gè)塊,比如if語(yǔ)句,新的變量會(huì)在塊作用域里聲明,不會(huì)對(duì)外面作用域產(chǎn)生印象。這不適用于JavaScript。在Firebug里運(yùn)行下面的代碼:

var x = 1;
console.log(x); // 1
if (true) {
    var x = 2;
    console.log(x); // 2
}
console.log(x); // 2

在這個(gè)例子中,F(xiàn)irebug將輸出1,2,2。這是因?yàn)镴avaScript有函數(shù)級(jí)作用域(function-level scope)。這一點(diǎn)和C家族完全不同。語(yǔ)句塊,如if語(yǔ)言,不創(chuàng)建新的作用域。僅僅函數(shù)創(chuàng)建新作用域。

很多程序員,像C,C++,C#或Java,都不知道這點(diǎn),也不希望這樣。幸運(yùn)的是,因?yàn)镴avaScript函數(shù)的靈活性,有一個(gè)解決方案。你若你必須要在函數(shù)內(nèi)部創(chuàng)建一個(gè)臨時(shí)作用域,像下面這樣做:

function foo() {
    var x = 1;
    if (x) {
        (function () {
            var x = 2;
            // 此處省略一萬(wàn)個(gè)字
        }());
    }
    // x 仍然是 1.
}

這方法實(shí)際上相當(dāng)靈活,可以在你需要臨時(shí)作用域的時(shí)候隨意使用,不局限于塊級(jí)語(yǔ)句內(nèi)部。然而,我強(qiáng)烈建議你花時(shí)間去了解和欣賞JavaScript的作用域。它非常強(qiáng)大,是這門(mén)語(yǔ)言中我最喜歡的特性之一。如果你了解作用域,將更容易理解提升。

聲明,名字和提升(Hoisting)

在JavaScript中,作用域中的名字(屬性名)有四種基本來(lái)源:

  1. 語(yǔ)言定義:默認(rèn)所有作用域都有屬性名this和arguments。
  2. 形參:函數(shù)可能有形式參數(shù),其作用域是整個(gè)函數(shù)體內(nèi)部。
  3. 函數(shù)聲明:類(lèi)似于function foo() {}這種形式。
  4. 變量聲明:var foo;這種形式的代碼。 函數(shù)聲明和變量聲明總是被JavaScript解釋器無(wú)形中移動(dòng)到(提升)包含他們的作用域頂部。函數(shù)參數(shù)和語(yǔ)言定義的名稱(chēng)明顯總是存在。這意味著像下面的代碼:

    function foo() { bar(); var x = 1; }

實(shí)際上被解釋為像下面這樣:

function foo() {
    var x;
    bar();
    x = 1;
}

無(wú)論包含聲明的代碼行是否會(huì)被執(zhí)行,上面的過(guò)程都會(huì)發(fā)生。下面的兩個(gè)函數(shù)是等價(jià)的:

function foo() {
    if (false) {
        var x = 1;
    }
    return;
    var y = 1;
}
function foo() {
    var x, y;
    if (false) {
        x = 1;
    }
    return;
    y = 1;
}

注意變量聲明中賦值的過(guò)程不會(huì)被提升。僅僅變量名字被提升了。這不適用于函數(shù)聲明,整個(gè)函數(shù)體也會(huì)提升。但不要忘記有兩種聲明函數(shù)的方法。考慮下面的JavaScript代碼:

function test() {
    foo(); // 類(lèi)型錯(cuò)誤 “foo 不是一個(gè)函數(shù)”
    bar(); // “這能運(yùn)行”
    var foo = function () { // 將函數(shù)表達(dá)式賦值給本地變量“foo”
        alert("this won't run!");
    }
    function bar() { //  'bar'函數(shù)聲明,分配“bar”名字
        alert("this will run!");
    }
}
test();

在這種情況下,僅僅函數(shù)聲明的函數(shù)體被提升到頂部。名字“foo”被提升,但后面的函數(shù)體,在執(zhí)行的時(shí)候才被指派。

這是全部的基本提升,看起來(lái)并不復(fù)雜和讓人混淆。當(dāng)然,這是JavaScript,在某些特殊性況下會(huì)更復(fù)雜一點(diǎn)。

名字解析順序

需要記住的最重要的特殊情況是名字的解析順序。記住作用域中的名字有四種來(lái)源。上面我列出他們的順序是他們被解析的順序。一般來(lái)說(shuō),如果一個(gè)名字已經(jīng)被定義過(guò),那么它不會(huì)在被其他有相同名字的屬性重寫(xiě)。這意味著函數(shù)聲明優(yōu)先于變量聲明。這并不意味著為名字賦值的過(guò)程將不工作,僅僅聲明的過(guò)程會(huì)被忽略。有幾個(gè)例外情況:

  • 函數(shù)的內(nèi)置變量arguments比較奇怪。它看起來(lái)是在普通的函數(shù)參數(shù)之后才聲明,其實(shí)是在函數(shù)聲明之前。如果參數(shù)里面有名稱(chēng)為arguments的參數(shù),它會(huì)比內(nèi)置的那個(gè)優(yōu)先級(jí)高,即使它是undefined。所以不要使用arguments作為為函數(shù)參數(shù)的名稱(chēng)。
  • 嘗試使用this作為標(biāo)示符的地方都會(huì)造成一個(gè)語(yǔ)法錯(cuò)誤。這是一個(gè)很好的特性。
  • 如果多個(gè)參數(shù)具有相同的名字,那么最后一個(gè)參數(shù)會(huì)優(yōu)先于先前的,即使它是undefined。

命名函數(shù)表達(dá)式

你可以在函數(shù)表達(dá)式給中給函數(shù)命名,用這樣的語(yǔ)法不能完成一個(gè)函數(shù)聲明,下面有一些代碼來(lái)說(shuō)明我的意思:

foo(); // TypeError "foo is not a function"
bar(); // valid
baz(); // TypeError "baz is not a function"
spam(); // ReferenceError "spam is not defined"

var foo = function () {}; // 匿名函數(shù)表達(dá)式(“foo”會(huì)被提升)
function bar() {}; // 函數(shù)聲明(“bar”和函數(shù)體會(huì)被提升)
var baz = function spam() {}; // 命名函數(shù)表達(dá)式(僅“baz”會(huì)被提升)

foo(); // valid
bar(); // valid
baz(); // valid
spam(); // ReferenceError "spam is not defined"

編碼時(shí)如何使用這些知識(shí)

現(xiàn)在你應(yīng)該理解了作用域和提升(hoisting),那么我們?cè)诰帉?xiě)JavaScript的時(shí)候應(yīng)該怎么做呢?最重要的事情就是始終用var表達(dá)式來(lái)聲明你的變量。我強(qiáng)烈建議你使用單var模式(single var)。如果你強(qiáng)迫自己做到這一點(diǎn),你將永遠(yuǎn)不會(huì)遇到任何與變量提升相關(guān)的混亂的問(wèn)題。但是這樣做也讓我們很難跟蹤那些在當(dāng)前作用域中實(shí)際上已經(jīng)聲明的變量。我建議你使用JSLint和聲明一次原則來(lái)進(jìn)行實(shí)際操作,如果你這樣做了,你的代碼應(yīng)該會(huì)看起來(lái)像這樣:

/*jslint onevar: true [...] */
function foo(a, b, c) {
    var x = 1,
        bar,
        baz = "something";
}

標(biāo)準(zhǔn)給出的解釋

我翻了翻ECMAScript標(biāo)準(zhǔn),想直接了解這些東西是如何工作的,發(fā)現(xiàn)效果不錯(cuò)。這里我不得不說(shuō)關(guān)于變量聲明和作用域(第12.2.2節(jié))的內(nèi)容:

如果在一個(gè)函數(shù)中聲明變量,這些變量就被定義在了在該函數(shù)的函數(shù)作用域中,見(jiàn)第10.1.3所述。不然它們就是被定義在全局的作用域內(nèi)(即,它們被創(chuàng)建為全局對(duì)象的成員,見(jiàn)第10.1.3所述),當(dāng)進(jìn)入執(zhí)行環(huán)境的時(shí)候,變量就被創(chuàng)建。一個(gè)語(yǔ)句塊不能定義一個(gè)新的作用域。只有一個(gè)程序或者函數(shù)聲明能夠產(chǎn)生一個(gè)新的作用域。創(chuàng)建變量時(shí),被初始化為undefined。如果變量聲明語(yǔ)句里面帶有賦值操作,則賦值操作只有被執(zhí)行到聲明語(yǔ)句的時(shí)候才會(huì)發(fā)生,而不是創(chuàng)建的時(shí)候。

我希望這篇文章闡明了對(duì)JavaScript程序員來(lái)說(shuō)最常見(jiàn)的迷惑問(wèn)題,我試圖講的盡可能詳盡,以避免造成更多的迷惑,如果我說(shuō)錯(cuò)了或者有大的遺漏,請(qǐng)通知我。

原文 http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)