鍍金池/ 教程/ HTML/ 《你真懂 JavaScript 嗎?》答案詳解
代碼復(fù)用模式(避免篇)
S.O.L.I.D 五大原則之接口隔離原則 ISP
設(shè)計模式之狀態(tài)模式
JavaScript 核心(晉級高手必讀篇)
設(shè)計模式之建造者模式
JavaScript 與 DOM(上)——也適用于新手
設(shè)計模式之中介者模式
設(shè)計模式之裝飾者模式
設(shè)計模式之模板方法
設(shè)計模式之外觀模式
強大的原型和原型鏈
設(shè)計模式之構(gòu)造函數(shù)模式
揭秘命名函數(shù)表達式
深入理解J avaScript 系列(結(jié)局篇)
執(zhí)行上下文(Execution Contexts)
函數(shù)(Functions)
《你真懂 JavaScript 嗎?》答案詳解
設(shè)計模式之適配器模式
設(shè)計模式之組合模式
設(shè)計模式之命令模式
S.O.L.I.D 五大原則之單一職責(zé) SRP
編寫高質(zhì)量 JavaScript 代碼的基本要點
求值策略
閉包(Closures)
對象創(chuàng)建模式(上篇)
This? Yes,this!
設(shè)計模式之代理模式
變量對象(Variable Object)
S.O.L.I.D 五大原則之里氏替換原則 LSP
面向?qū)ο缶幊讨话憷碚?/span>
設(shè)計模式之單例模式
Function 模式(上篇)
S.O.L.I.D 五大原則之依賴倒置原則 DIP
設(shè)計模式之迭代器模式
立即調(diào)用的函數(shù)表達式
設(shè)計模式之享元模式
設(shè)計模式之原型模式
根本沒有“JSON 對象”這回事!
JavaScript 與 DOM(下)
面向?qū)ο缶幊讨?ECMAScript 實現(xiàn)
全面解析 Module 模式
對象創(chuàng)建模式(下篇)
設(shè)計模式之職責(zé)鏈模式
S.O.L.I.D 五大原則之開閉原則 OCP
設(shè)計模式之橋接模式
設(shè)計模式之策略模式
設(shè)計模式之觀察者模式
代碼復(fù)用模式(推薦篇)
作用域鏈(Scope Chain)
Function 模式(下篇)
設(shè)計模式之工廠模式

《你真懂 JavaScript 嗎?》答案詳解

介紹

昨天發(fā)的《大叔手記(19):你真懂 JavaScript 嗎?》里面的 5 個題目,有很多回答,發(fā)現(xiàn)強人還是很多的,很多人都全部答對了。

今天我們來對這 5 個題目詳細分析一下,希望對大家有所幫助。

題目1

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,而不是不可訪問。

題目 2

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)容。

題目 3

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)容了。

題目 4

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 對象是活動對象的一個屬性,它包括如下屬性:

  1. callee — 指向當(dāng)前函數(shù)的引用
  2. length — 真正傳遞的參數(shù)個數(shù)
  3. properties-indexes (字符串類型的整數(shù)) 屬性的值就是函數(shù)的參數(shù)值(按參數(shù)列表從左到右排列)。 properties-indexes 內(nèi)部元素的個數(shù)等于 arguments.length. properties-indexes 的值和實際傳遞進來的參數(shù)之間是共享的。
  4. 這個共享其實不是真正的共享一個內(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);

題目 5

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]就很容易理解了。

總結(jié)

這 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ǔ)核心特性的理解。

  1. 找出數(shù)字數(shù)組中最大的元素(使用 Match.max 函數(shù))
  2. 轉(zhuǎn)化一個數(shù)字數(shù)組為 function 數(shù)組(每個 function 都彈出相應(yīng)的數(shù)字)
  3. 給 object 數(shù)組進行排序(排序條件是每個元素對象的屬性個數(shù))
  4. 利用 JavaScript 打印出 Fibonacci 數(shù)(不使用全局變量)
  5. 實現(xiàn)如下語法的功能:var a = (5).plus(3).minus(6); //2
  6. 實現(xiàn)如下語法的功能:var a = add(2)(3)(4); //9