到目前為止,大家應(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ù))都會被“移動”到各自的作用域的最頂端,這個過程叫作提升。