JS學(xué)習(xí)系列 04 - 提升

到目前為止,大家應(yīng)該很熟悉作用域的概念了,以及根據(jù)聲明的位置和方式將變量分配給作用域的相關(guān)原理了。函數(shù)作用域和塊作用域的行為是一樣的,可以總結(jié)為:任何聲明在某個作用域內(nèi)的變量,都將屬于這個作用域。

但是作用域同其中的變量聲明出現(xiàn)的位置有某種微妙的關(guān)系,而這個細節(jié)就是我們這節(jié)要探討的內(nèi)容。

1. 聲明提升

先看代碼:

a = 2;

var a;

console.log(a);

大家認為這里會輸出什么?

有一些人認為是 undefined ,因為 var a; 是在 a = 2; 之后,所以會覺得 undefined 覆蓋了 a 的值。但是,真正的結(jié)果是 2 。

再看一段代碼:

console.log(a);

var a = 2;

鑒于上一個例子,有些人會認為這里會輸出 2 ,也有人認為由于 a 在使用前并沒有聲明,所以這里會報錯。但是,這里的結(jié)果是 undefined 。

之前討論編譯器的時候,我們知道 JS 引擎會在解釋代碼之前首先對其進行編譯。編譯階段的第一部分工作就是找到所有的聲明,并用合適的作用域?qū)⑺鼈冴P(guān)聯(lián)起來。

因此,正確的思路是,包括變量和函數(shù)在內(nèi)的所有聲明都會在任何代碼執(zhí)行前首先被處理。

當你看到 var a = 2; 時,JavaScript 實際上會將其看成兩個聲明:var a;a = 2; 。第一個定義聲明是在編譯階段進行的。第二個賦值聲明會被留在原地等待執(zhí)行階段。

所以,在第一個例子中,代碼的等價形式是這樣的:

var a;

a = 2;

console.log(a);

第二個例子中,代碼的等價形式是這樣的:

var a;

console.log(a);

a = 2;

這個過程就好像是變量和函數(shù)聲明從它們的代碼中出現(xiàn)的位置被“移動”到了最上面。這個過程就叫作“提升”。

注意,只有聲明本身會被提升,而賦值操作和其他運行邏輯都會停留在原地,想象一下,如果提升會改變代碼的執(zhí)行順序,那么會造成非常嚴重的破壞。

還有一點,函數(shù)聲明會被提升,但是函數(shù)表達式不會被提升。

foo();      // 報錯,TypeError: foo is not a function,因為這里 foo 是 undefined,并不是一個函數(shù)

var foo = function foo() {
    // something else
}

這段程序中的變量標識符 foo 被提升并分配給所在的作用域(在這里是全局作用域),因此 foo() 不會導(dǎo)致 ReferenceError 。但是,foo 此時并沒有賦值(如果它是一個函數(shù)聲明而不是函數(shù)表達式,那么就會被賦值)。foo() 由于對 undefined 值進行函數(shù)調(diào)用而導(dǎo)致非法操作,所以會拋出 TypeError 異常。

同時,即使是具名函數(shù)表達式,名稱標識符在賦值之前也無法在所在作用域中使用:

foo();
bar();

var foo = function bar () {
    // something else
};

這段代碼經(jīng)過提升后,實際上等價于:

var foo;

foo();
bar();

foo = function () {
    var bar = ...self...
    // something else
};

2. 函數(shù)優(yōu)先

函數(shù)聲明和變量聲明都會被提升。但是一個值得注意的細節(jié)是,函數(shù)聲明會首先被提升,然后才是變量。

考慮如下代碼:

foo();      // 1

var foo;

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

foo = function () {
   console.log(2);
};

這里會輸出 1 而不是 2 。這段代碼其實等價于:

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

foo();      // 1

foo = function () {
   console.log(2);
};

var foo; 盡管出現(xiàn)在 function foo() {...} 聲明之前,但是它是重復(fù)聲明,所以會被編譯器忽略,因為函數(shù)聲明會被提升到變量聲明之前。

注意,盡管重復(fù)的 var 聲明會被忽略,但重復(fù)的函數(shù)聲明卻會覆蓋前一個同名函數(shù)。

foo();      // 3

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

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

foo();        // 2

function foo () {
   cosole.log(3);
}

這個例子充分說明了在同一個作用域中進行重復(fù)定義是非常糟糕的,而且經(jīng)常會導(dǎo)致各種奇怪的問題。上面那個例子,等價于:

function foo () {
   cosole.log(3);
}

foo();      // 3

foo = function () {
   console.log(2);
};      

foo();        // 2

還有一些人會犯如下錯誤:

foo();      // 2

var a = true;

if (a) {
   function foo () {
      console.log(1);
   }
} else {
   function foo () {
      console.log(2);
   }
}

因為 if 并沒有塊作用域,所以這里的函數(shù)聲明會提升到其作用域最前邊,而后一個 function 聲明會覆蓋前一個,所以這里結(jié)果是 2 。這里代碼等價如下:

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

var a;

foo();      // 2

a = true;

if (a) {

} else {

}

3. 總結(jié)

我們習(xí)慣將 var a = 2; 看作一個聲明,而實際上 JavaScript 引擎并不這么認為。它將 var a;a = 2; 當作兩個單獨的聲明,第一個是編譯階段的任務(wù),而第二個則是執(zhí)行階段的任務(wù)。

這意味著無論作用域中的聲明出現(xiàn)在什么地方,都將在代碼本身被執(zhí)行前首先被處理(預(yù)編譯)??梢詫⑦@個過程想象成所有的聲明(變量和函數(shù))都會被“移動”到各自的作用域的最頂端,這個過程叫作提升。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 繼承 一、混入式繼承 二、原型繼承 利用原型中的成員可以被和其相關(guān)的對象共享這一特性,可以實現(xiàn)繼承,這種實現(xiàn)繼承的...
    magic_pill閱讀 1,124評論 0 3
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點點福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運大...
    HetfieldJoe閱讀 4,064評論 12 65
  • let 命令 塊級作用域 const 命令 頂層對象的屬性 global 對象 let 命令 基本用法 ES6 新...
    嘉奇呦_nice閱讀 1,692評論 0 2
  • You don't KnowJS 引語:你不懂的JS這本書?github上已經(jīng)有了7w的star最近也是張野大大給...
    Sleet閱讀 661評論 0 0
  • 前記:對于鄧城豬蹄,早已肯定了它的美味。只因早前的一篇文章《安陽:安靜的月光》被收錄簡書專題《河南省》,臨時起意,...
    蘇拓閱讀 1,083評論 0 1

友情鏈接更多精彩內(nèi)容