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