深入理解JavaScript系列(20):《你真懂JavaScript嗎?》答案詳解

來源:互聯網
上載者:User
介紹

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

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

註:

問題來自大名鼎鼎的前端架構師Baranovskiy的文章《So, you think you know JavaScript?》。
答案也是來自大名鼎鼎的JS牛人Nicholas C. Zakas的文章《Answering Baranovskiy’s JavaScript quiz》——《JavaScript進階程式設計》一書的原作者
(但題目2的解釋貌似有點問題)

OK,我們先看第一題

題目1
if (!("a" in window)) {
var a = 1;
}
alert(a);

代碼看起來是想說:如果window不包含屬性a,就聲明一個變數a,然後賦值為1。

你可能認為alert出來的結果是1,然後實際結果是“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結果是true了。

第三,你需要理解該題目的意思是,變數聲明被提前了,但變數賦值沒有,因為這行程式碼封裝括了變數聲明和變數賦值。

你可以將語句拆分為如下代碼:

var a;    //聲明
a = 1; //初始化賦值

當變數聲明和賦值在一起用的時候,JavaScript引擎會自動將它分為兩部以便將變數聲明提前,不將賦值的步驟提前是因為他有可能影響代碼執行出不可預期的結果。

所以,知道了這些概念以後,重新回頭看一下題目的代碼,其實就等價於:

var a;
if (!("a" in window)) {
a = 1;
}
alert(a);

這樣,題目的意思就非常清楚了:首先聲明a,然後判斷a是否在存在,如果不存在就賦值為1,很明顯a永遠在window裡存在,這個指派陳述式永遠不會執行,所以結果是undefined。

大叔註:提前這個詞語顯得有點迷惑了,其實就是執行內容的關係,因為執行內容分2個階段:進入執行內容和執行代碼,在進入執行內容的時候,建立變數對象VO裡已經有了:函數的所有形參、所有的函式宣告、所有的變數聲明

VO(global) = {
a: undefined
}

這個時候a已經有了;

然後執行代碼的時候才開始走if語句,詳細資料請查看《深入理解JavaScript系列(12):變數對象(Variable Object)》中的處理上下文代碼的2個階段小節。

大叔註:相信很多人都是認為a在裡面不可訪問,結果才是undefined的吧,其實是已經有了,只不過初始值是undefined,而不是不可訪問。

題目2
var a = 1,
b = function a(x) {
x && a(--x);
};
alert(a);

這個題目看起來比實際複雜,alert的結果是1;這裡依然有3個重要的概念需要我們知道。

首先,在題目1裡我們知道了變數聲明在進入執行內容就完成了;第二個概念就是函式宣告也是提前的,所有的函式宣告都在執行代碼之前都已經完成了聲明,和變

量聲明一樣。澄清一下,函式宣告是如下這樣的代碼:

function functionName(arg1, arg2){
//函數體
}

如下不是函數,而是函數運算式,相當於變數賦值:

var functionName = function(arg1, arg2){
//函數體
};

澄清一下,函數運算式沒有提前,就相當於平時的變數賦值。

第三需要知道的是,函式宣告會覆蓋變數聲明,但不會覆蓋變數賦值,為瞭解釋這個,我們來看一個例子:

function value(){
return 1;
}
var value;
alert(typeof value); //"function"

儘快變數聲明在下面定義,但是變數value依然是function,也就是說這種情況下,函式宣告的優先順序高於變數聲明的優先順序,但如果該變數value賦值了,那結果就完全不一樣了:

function value(){
return 1;
}
var value = 1;
alert(typeof value); //"number"

該value賦值以後,變數賦值初始化就覆蓋了函式宣告。

重新回到題目,這個函數其實是一個有名函數運算式,函數運算式不像函式宣告一樣可以覆蓋變數聲明,但你可以注意到,變數b是包含了該函數運算式,而 該函數運算式的名字是a;不同的瀏覽器對a這個名詞處理有點不一樣,在IE裡,會將a認為函式宣告,所以它被變數初始化覆蓋了,就是說如果調用 a(--x)的話就會出錯,而其它瀏覽器在允許在函數內部調用a(--x),因為這時候a在函數外面依然是數字。基本上,IE裡調用b(2)的時候會出 錯,但其它瀏覽器則返回undefined。

理解上述內容之後,該題目換成一個更準確和更容易理解的代碼應該像這樣:

var a = 1,
b = function(x) {
x && b(--x);
};
alert(a);

這樣的話,就很清晰地知道為什麼alert的總是1了,詳細內容請參考《深入理解JavaScript系列(2):揭秘命名函數運算式》中的內容。

大叔註:安裝ECMAScript規範,作者對函式宣告覆蓋變數聲明的解釋其實不準確的,正確的理解應該是如下:

進入執行內容: 這裡出現了名字一樣的情況,一個是函數申明,一個是變數申明。那麼,根據深入理解JavaScript系列(12):變數對象(Variable Object)介紹的,填充VO的順序是: 函數的形參 -> 函數申明 -> 變數申明。
上述例子中,變數a在函數a後面,那麼,變數a遇到函數a怎麼辦呢?還是根據變數對象中介紹的,當變數申明遇到VO中已經有同名的時候,不會影響已經存在的屬性。而函數運算式不會影響VO的內容,所以b只有在執行的時候才會觸發裡面的內容。

題目3
function a(x) {
return x * 2;
}
var a;
alert(a);

這個題目就是題目2裡的大叔加的注釋了,也就是函式宣告和變數聲明的關係和影響,遇到同名的函式宣告,VO不會重新定義,所以這時候全域的VO應該是如下這樣的:

VO(global) = {
a: 引用了函式宣告“a”
}

而執行a的時候,相應地就彈出了函數a的內容了。

題目4
function b(x, y, a) {
arguments[2] = 10;
alert(a);
}
b(1, 2, 3);

關於這個題目,NC搬出了262-3的規範出來解釋,其實從《深入理解JavaScript系列(12):變數對象(Variable Object)》中的函數上下文中的變數對象一節就可以清楚地知道,使用中的物件是在進入函數上下文時刻被建立的,它通過函數的arguments屬性初始化。arguments屬性的值是Arguments對象:

AO = {
arguments: <ArgO>
};

Arguments對象是使用中的物件的一個屬性,它包括如下屬性:

  1. callee — 指向當前函數的引用
  2. length — 真正傳遞的參數個數
  3. properties-indexes (字串類型的整數) 屬性的值就是函數的參數值(按參數列表從左至右排列)。 properties-indexes內部元素的個數等於arguments.length. properties-indexes 的值和實際傳遞進來的參數之間是共用的。

這個共用其實不是真正的共用一個記憶體位址,而是2個不同的記憶體位址,使用JavaScript引擎來保證2個值是隨時一樣的,當然這也有一個前提, 那就是這個索引值要小於你傳入的參數個數,也就是說如果你只傳入2個參數,而還繼續使用arguments[2]賦值的話,就會不一致,例如:

function b(x, y, a) {
arguments[2] = 10;
alert(a);
}
b(1, 2);

這時候因為沒傳遞第三個參數a,所以賦值10以後,alert(a)的結果依然是undefined,而不是10,但如下代碼彈出的結果依然是10,因為和a沒有關係。

function b(x, y, a) {
arguments[2] = 10;
alert(arguments[2]);
}
b(1, 2);

題目5
function a() {
alert(this);
}
a.call(null);

這個題目可以說是最簡單的,也是最詭異的,因為如果沒學到它的定義的話,打死也不會知道結果的,關於這個題目,我們先來瞭解2個概念。

首先,就是this值是如何定義的,當一個方法在對象上調用的時候,this就指向到了該對象上,例如:

var object = {
method: function() {
alert(this === object); //true
}
}
object.method();

上面的代碼,調用method()的時候this被指向到調用它的object對象上,但在全域範圍裡,this是等價於window(瀏覽器 中,非瀏覽器裡等價於global),在如果一個function的定義不是屬於一個對象屬性的時候(也就是單獨定義的函數),函數內部的this也是等 價於window的,例如:

function method() {
alert(this === window); //true
}
method();

瞭解了上述概念之後,我們再來瞭解一下call()是做什麼的,call方法作為一個function執行代表該方法可以讓另外一個對象作為調用者來調用,call方法的第一個參數是對象調用者,隨後的其它參數是要傳給調用method的參數(如果聲明了的話),例如:

function method() {
alert(this === window);
}
method(); //true
method.call(document); //false

第一個依然是true沒什麼好說的,第二個傳入的調用對象是document,自然不會等於window,所以彈出了false。

另外,根據ECMAScript262規範規定:如果第一個參數傳入的對象調用者是null或者undefined的話,call方法將把全域對象 (也就是window)作為this的值。所以,不管你什麼時候傳入null,其this都是全域對象window,所以該題目可以理解成如下代碼:

function a() {
alert(this);
}
a.call(window);

所以彈出的結果是[object Window]就很容易理解了。

總結

這5個題目雖然貌似有點偏,但實際上考察的依然是基本概念,只有熟知了這些基本概念才能寫出高品質代碼。

關於JavaScript的基本核心內容和理解基本上在該系列就到此為止了,接下來的章節除了把五大原則剩餘的2篇補全依然,會再加兩篇關於DOM 的文章,然後就開始轉向整理關於JavaScript模式與設計模式相關的文章了(大概10篇左右),隨後再會花幾個章節來一個實戰系列。

更多題目

如果大家有興趣,可以繼續研究下面的一些題目,詳細通過這些題目也可以再次加深對JavaScript基礎核心特性的理解。

大叔註:這些題目也是來自出這5個題目的人,當然如果你能答對4個及以上並且想拿高工資的話,請聯絡我。

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

轉自:湯姆大叔

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.