昨天發(fā)的《大叔手記(19):你真懂 JavaScript 嗎?》里面的 5 個題目,有很多回答,發(fā)現(xiàn)強人還是很多的,很多人都全部答對了。
今天我們來對這 5 個題目詳細分析一下,希望對大家有所幫助。
if (!("a" in window)) {
var a = 1;
}
alert(a);
代碼看起來是想說:如果 window 不包含屬性 a,就聲明一個變量 a,然后賦值為 1。
你可能認為 alert 出來的結(jié)果是 1,然后實際結(jié)果是“undefined”。要了解為什么,我們需要知道 JavaScript 里的 3 個概念。
首先,所有的全局變量都是 window 的屬性,語句 var a = 1;等價于 window.a = 1;你可以用如下方式來檢測全局變量是否聲明:
"變量名稱" in window
第二,所有的變量聲明都在范圍作用域的頂部,看一下相似的例子:
alert("a" in window);
var a;
此時,盡管聲明是在 alert 之后,alert 彈出的依然是 true,這是因為 JavaScript 引擎首先會掃墓所有的變量聲明,然后將這些變量聲明移動到頂部,最終的代碼效果是這樣的:
var a;
alert("a" in window);
這樣看起來就很容易解釋為什么 alert 結(jié)果是 true 了。
第三,你需要理解該題目的意思是,變量聲明被提前了,但變量賦值沒有,因為這行代碼包括了變量聲明和變量賦值。
你可以將語句拆分為如下代碼:
var a; //聲明
a = 1; //初始化賦值
當(dāng)變量聲明和賦值在一起用的時候,JavaScript 引擎會自動將它分為兩部以便將變量聲明提前,不將賦值的步驟提前是因為他有可能影響代碼執(zhí)行出不可預(yù)期的結(jié)果。
所以,知道了這些概念以后,重新回頭看一下題目的代碼,其實就等價于:
var a;
if (!("a" in window)) {
a = 1;
}
alert(a);
這樣,題目的意思就非常清楚了:首先聲明a,然后判斷a是否在存在,如果不存在就賦值為 1,很明顯 a 永遠在 window 里存在,這個賦值語句永遠不會執(zhí)行,所以結(jié)果是 undefined。
大叔注:提前這個詞語顯得有點迷惑了,其實就是執(zhí)行上下文的關(guān)系,因為執(zhí)行上下文分 2 個階段:進入執(zhí)行上下文和執(zhí)行代碼,在進入執(zhí)行上下文的時候,創(chuàng)建變量對象 VO 里已經(jīng)有了:函數(shù)的所有形參、所有的函數(shù)聲明、所有的變量聲明
VO(global) = {
a: undefined
}
這個時候 a 已經(jīng)有了;
然后執(zhí)行代碼的時候才開始走if語句,詳細信息請查看《深入理解 JavaScript 系列(12):變量對象(Variable Object)》中的處理上下文代碼的 2 個階段小節(jié)。
大叔注:相信很多人都是認為 a 在里面不可訪問,結(jié)果才是 undefined 的吧,其實是已經(jīng)有了,只不過初始值是 undefined,而不是不可訪問。
var a = 1,
b = function a(x) {
x && a(--x);
};
alert(a);
這個題目看起來比實際復(fù)雜,alert 的結(jié)果是 1;這里依然有 3 個重要的概念需要我們知道。
首先,在題目 1 里我們知道了變量聲明在進入執(zhí)行上下文就完成了;第二個概念就是函數(shù)聲明也是提前的,所有的函數(shù)聲明都在執(zhí)行代碼之前都已經(jīng)完成了聲明,和變量聲明一樣。澄清一下,函數(shù)聲明是如下這樣的代碼:
function functionName(arg1, arg2){
//函數(shù)體
}
如下不是函數(shù),而是函數(shù)表達式,相當(dāng)于變量賦值:
var functionName = function(arg1, arg2){
//函數(shù)體
};
澄清一下,函數(shù)表達式?jīng)]有提前,就相當(dāng)于平時的變量賦值。
第三需要知道的是,函數(shù)聲明會覆蓋變量聲明,但不會覆蓋變量賦值,為了解釋這個,我們來看一個例子:
function value(){
return 1;
}
var value;
alert(typeof value); //"function"
盡快變量聲明在下面定義,但是變量 value 依然是 function,也就是說這種情況下,函數(shù)聲明的優(yōu)先級高于變量聲明的優(yōu)先級,但如果該變量 value 賦值了,那結(jié)果就完全不一樣了:
function value(){
return 1;
}
var value = 1;
alert(typeof value); //"number"
該 value 賦值以后,變量賦值初始化就覆蓋了函數(shù)聲明。
重新回到題目,這個函數(shù)其實是一個有名函數(shù)表達式,函數(shù)表達式不像函數(shù)聲明一樣可以覆蓋變量聲明,但你可以注意到,變量 b 是包含了該函數(shù)表達式,而該函數(shù)表達式的名字是 a;不同的瀏覽器對a這個名詞處理有點不一樣,在 IE 里,會將 a 認為函數(shù)聲明,所以它被變量初始化覆蓋了,就是說如果調(diào)用 a(--x)的話就會出錯,而其它瀏覽器在允許在函數(shù)內(nèi)部調(diào)用 a(--x),因為這時候 a 在函數(shù)外面依然是數(shù)字?;旧?,IE 里調(diào)用 b(2)的時候會出錯,但其它瀏覽器則返回 undefined。
理解上述內(nèi)容之后,該題目換成一個更準確和更容易理解的代碼應(yīng)該像這樣:
var a = 1,
b = function(x) {
x && b(--x);
};
alert(a);
這樣的話,就很清晰地知道為什么 alert 的總是 1 了,詳細內(nèi)容請參考《深入理解 JavaScript 系列(2):揭秘命名函數(shù)表達式》中的內(nèi)容。
大叔注:安裝 ECMAScript 規(guī)范,作者對函數(shù)聲明覆蓋變量聲明的解釋其實不準確的,正確的理解應(yīng)該是如下:
進入執(zhí)行上下文: 這里出現(xiàn)了名字一樣的情況,一個是函數(shù)申明,一個是變量申明。那么,根據(jù)深入理解 JavaScrip t系列(12):變量對象(Variable Object)介紹的,填充 VO 的順序是: 函數(shù)的形參 -> 函數(shù)申明 -> 變量申明。
上述例子中,變量 a 在函數(shù) a 后面,那么,變量 a 遇到函數(shù) a 怎么辦呢?還是根據(jù)變量對象中介紹的,當(dāng)變量申明遇到 VO 中已經(jīng)有同名的時候,不會影響已經(jīng)存在的屬性。而函數(shù)表達式不會影響 VO 的內(nèi)容,所以 b 只有在執(zhí)行的時候才會觸發(fā)里面的內(nèi)容。
function a(x) {
return x * 2;
}
var a;
alert(a);
這個題目就是題目 2 里的大叔加的注釋了,也就是函數(shù)聲明和變量聲明的關(guān)系和影響,遇到同名的函數(shù)聲明,VO 不會重新定義,所以這時候全局的 VO 應(yīng)該是如下這樣的:
VO(global) = {
a: 引用了函數(shù)聲明“a”
}
而執(zhí)行 a 的時候,相應(yīng)地就彈出了函數(shù) a 的內(nèi)容了。
function b(x, y, a) {
arguments[2] = 10;
alert(a);
}
b(1, 2, 3);
關(guān)于這個題目,NC 搬出了 262-3 的規(guī)范出來解釋,其實從《深入理解 JavaScript 系列(12):變量對象(Variable Object)》中的函數(shù)上下文中的變量對象一節(jié)就可以清楚地知道,活動對象是在進入函數(shù)上下文時刻被創(chuàng)建的,它通過函數(shù)的 arguments 屬性初始化。arguments 屬性的值是 Arguments 對象:
AO = {
arguments: <ArgO>
};
Arguments 對象是活動對象的一個屬性,它包括如下屬性:
共享
的。這個共享其實不是真正的共享一個內(nèi)存地址,而是 2 個不同的內(nèi)存地址,使用 JavaScript 引擎來保證 2 個值是隨時一樣的,當(dāng)然這也有一個前提,那就是這個索引值要小于你傳入的參數(shù)個數(shù),也就是說如果你只傳入 2 個參數(shù),而還繼續(xù)使用 arguments[2]賦值的話,就會不一致,例如:
function b(x, y, a) {
arguments[2] = 10;
alert(a);
}
b(1, 2);
這時候因為沒傳遞第三個參數(shù) a,所以賦值 10 以后,alert(a)的結(jié)果依然是 undefined,而不是 10,但如下代碼彈出的結(jié)果依然是 10,因為和 a 沒有關(guān)系。
function b(x, y, a) {
arguments[2] = 10;
alert(arguments[2]);
}
b(1, 2);
function a() {
alert(this);
}
a.call(null);
這個題目可以說是最簡單的,也是最詭異的,因為如果沒學(xué)到它的定義的話,打死也不會知道結(jié)果的,關(guān)于這個題目,我們先來了解 2 個概念。
首先,就是 this 值是如何定義的,當(dāng)一個方法在對象上調(diào)用的時候,this 就指向到了該對象上,例如:
var object = { method: function() { alert(this === object); //true } } object.method();
上面的代碼,調(diào)用 method() 的時候 this 被指向到調(diào)用它的 object 對象上,但在全局作用域里, this 是等價于 window(瀏覽器中,非瀏覽器里等價于 global),在如果一個 function 的定義不是屬于一個對象屬性的時候(也就是單獨定義的函數(shù)),函數(shù)內(nèi)部的 this 也是等價于 window 的,例如:
function method() {
alert(this === window); //true
}
method();
了解了上述概念之后,我們再來了解一下 call()是做什么的,call 方法作為一個 function 執(zhí)行代表該方法可以讓另外一個對象作為調(diào)用者來調(diào)用,call 方法的第一個參數(shù)是對象調(diào)用者,隨后的其它參數(shù)是要傳給調(diào)用 method 的參數(shù)(如果聲明了的話),例如:
function method() {
alert(this === window);
}
method(); //true
method.call(document); //false
第一個依然是 true 沒什么好說的,第二個傳入的調(diào)用對象是 document,自然不會等于 window,所以彈出了 false。
另外,根據(jù) ECMAScript262 規(guī)范規(guī)定:如果第一個參數(shù)傳入的對象調(diào)用者是 null 或者 undefined 的話,call 方法將把全局對象(也就是 window)作為 this 的值。所以,不管你什么時候傳入 null,其 this 都是全局對象 window,所以該題目可以理解成如下代碼:
function a() {
alert(this);
}
a.call(window);
所以彈出的結(jié)果是[object Window]就很容易理解了。
這 5 個題目雖然貌似有點偏,但實際上考察的依然是基本概念,只有熟知了這些基本概念才能寫出高質(zhì)量代碼。
關(guān)于 JavaScript 的基本核心內(nèi)容和理解基本上在該系列就到此為止了,接下來的章節(jié)除了把五大原則剩余的 2 篇補全依然,會再加兩篇關(guān)于 DOM 的文章,然后就開始轉(zhuǎn)向整理關(guān)于 JavaScript 模式與設(shè)計模式相關(guān)的文章了(大概10篇左右),隨后再會花幾個章節(jié)來一個實戰(zhàn)系列。
如果大家有興趣,可以繼續(xù)研究下面的一些題目,詳細通過這些題目也可以再次加深對 JavaScript 基礎(chǔ)核心特性的理解。