1.使用 typeof bar === "object"
來確定 bar
是否是物件的潛在陷阱是什麼?如何避免這個陷阱?
儘管 typeof bar === "object"
是檢查 bar
是否物件的可靠方法,令人驚訝的是在JavaScript中 null
也被認為是物件!
因此,令大多數開發人員驚訝的是,下面的程式碼將輸出 true
(而不是false
) 到控制檯:
1 2 |
var bar = null; console.log(typeof bar === "object"); // logs true! |
只要清楚這一點,同時檢查 bar
是否為 null
,就可以很容易地避免問題:
1 |
console.log((bar !== null) && (typeof bar === "object")); // logs false |
要答全問題,還有其他兩件事情值得注意:
首先,上述解決方案將返回 false
,當 bar
是一個函式的時候。在大多數情況下,這是期望行為,但當你也想對函式返回 true
的話,你可以修改上面的解決方案為:
1 |
console.log((bar !== null) && ((typeof bar === "object") || (typeof bar === "function"))); |
第二,上述解決方案將返回 true
,當 bar
是一個陣列(例如,當 var bar = [];
)的時候。在大多數情況下,這是期望行為,因為陣列是真正的物件,但當你也想對陣列返回 false
時,你可以修改上面的解決方案為:
1 |
console.log((bar !== null) && (typeof bar === "object") && (toString.call(bar) !== "[object Array]")); |
或者,如果你使用jQuery的話:
1 |
console.log((bar !== null) && (typeof bar === "object") && (! $.isArray(bar))); |
2.下面的程式碼將輸出什麼到控制檯,為什麼?
1 2 3 4 5 6 |
(function(){ var a = b = 3; })(); console.log("a defined? " + (typeof a !== 'undefined')); console.log("b defined? " + (typeof b !== 'undefined')); |
由於 a
和 b
都定義在函式的封閉範圍內,並且都始於 var
關鍵字,大多數JavaScript開發人員期望 typeof a
和 typeof b
在上面的例子中都是undefined。
然而,事實並非如此。這裡的問題是,大多數開發人員將語句 var a = b = 3;
錯誤地理解為是以下宣告的簡寫:
1 2 |
var b = 3; var a = b; |
但事實上,var a = b = 3;
實際是以下宣告的簡寫:
1 2 |
b = 3; var a = b; |
因此(如果你不使用嚴格模式的話),該程式碼段的輸出是:
1 2 |
a defined? false b defined? true |
但是, b
如何才能被定義在封閉函式的範圍之外呢?是的,既然語句 var a = b = 3;
是語句 b = 3;
和 var a = b;
的簡寫, b
最終成為了一個全域性變數(因為它沒有字首 var
關鍵字),因此仍然在範圍內甚至封閉函式之外。
需要注意的是,在嚴格模式下(即使用 use strict
),語句var a = b = 3;
將生成ReferenceError: b is not defined
的執行時錯誤,從而避免任何否則可能會導致的headfakes /bug。 (還是你為什麼應該理所當然地在程式碼中使用 use strict
的最好例子!)
3.下面的程式碼將輸出什麼到控制檯,為什麼?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var myObject = { foo: "bar", func: function() { var self = this; console.log("outer func: this.foo = " + this.foo); console.log("outer func: self.foo = " + self.foo); (function() { console.log("inner func: this.foo = " + this.foo); console.log("inner func: self.foo = " + self.foo); }()); } }; myObject.func(); |
上面的程式碼將輸出以下內容到控制檯:
1 2 3 4 |
outer func: this.foo = bar outer func: self.foo = bar inner func: this.foo = undefined inner func: self.foo = bar |
在外部函式中, this
和self
兩者都指向了 myObject
,因此兩者都可以正確地引用和訪問 foo
。
在內部函式中, this
不再指向 myObject
。其結果是,this.foo
沒有在內部函式中被定義,相反,指向到本地的變數self
保持在範圍內,並且可以訪問。 (在ECMA 5之前,在內部函式中的this
將指向全域性的 window
物件;反之,因為作為ECMA 5,內部函式中的功能this
是未定義的。)
4.封裝JavaScript原始檔的全部內容到一個函式塊有什麼意義及理由?
這是一個越來越普遍的做法,被許多流行的JavaScript庫(jQuery,Node.js等)採用。這種技術建立了一個圍繞檔案全部內容的閉包,也許是最重要的是,建立了一個私有的名稱空間,從而有助於避免不同JavaScript模組和庫之間潛在的名稱衝突。
這種技術的另一個特點是,允許一個易於引用的(假設更短的)別名用於全域性變數。這通常用於,例如,jQuery外掛中。jQuery允許你使用jQuery.noConflict()
,來禁用 $
引用到jQuery名稱空間。在完成這項工作之後,你的程式碼仍然可以使用$
利用這種閉包技術,如下所示:
1 |
(function($) { /* jQuery plugin code referencing $ */ } )(jQuery); |
5.在JavaScript原始檔的開頭包含 use strict
有什麼意義和好處?
對於這個問題,既簡要又最重要的答案是,use strict
是一種在JavaScript程式碼執行時自動實行更嚴格解析和錯誤處理的方法。那些被忽略或默默失敗了的程式碼錯誤,會產生錯誤或丟擲異常。通常而言,這是一個很好的做法。
嚴格模式的一些主要優點包括:
- 使除錯更加容易。那些被忽略或默默失敗了的程式碼錯誤,會產生錯誤或丟擲異常,因此儘早提醒你程式碼中的問題,你才能更快地指引到它們的原始碼。
- 防止意外的全域性變數。如果沒有嚴格模式,將值分配給一個未宣告的變數會自動建立該名稱的全域性變數。這是JavaScript中最常見的錯誤之一。在嚴格模式下,這樣做的話會丟擲錯誤。
- 消除
this
強制。如果沒有嚴格模式,引用null或未定義的值到this
值會自動強制到全域性變數。這可能會導致許多令人頭痛的問題和讓人恨不得拔自己頭髮的bug。在嚴格模式下,引用 null或未定義的this
值會丟擲錯誤。 - 不允許重複的屬性名稱或引數值。當檢測到物件(例如,
var object = {foo: "bar", foo: "baz"};
)中重複命名的屬性,或檢測到函式中(例如,function foo(val1, val2, val1){}
)重複命名的引數時,嚴格模式會丟擲錯誤,因此捕捉幾乎可以肯定是程式碼中的bug可以避免浪費大量的跟蹤時間。 - 使
eval()
更安全。在嚴格模式和非嚴格模式下,eval()
的行為方式有所不同。最顯而易見的是,在嚴格模式下,變數和宣告在eval()
語句內部的函式不會在包含範圍內建立(它們會在非嚴格模式下的包含範圍中被建立,這也是一個常見的問題源)。 - 在
delete
使用無效時丟擲錯誤。delete
操作符(用於從物件中刪除屬性)不能用在物件不可配置的屬性上。當試圖刪除一個不可配置的屬性時,非嚴格程式碼將默默地失敗,而嚴格模式將在這樣的情況下丟擲異常。
6.考慮以下兩個函式。它們會返回相同的東西嗎? 為什麼相同或為什麼不相同?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function foo1() { return { bar: "hello" }; } function foo2() { return { bar: "hello" }; } |
出人意料的是,這兩個函式返回的內容並不相同。更確切地說是:
1 2 3 4 |
console.log("foo1 returns:"); console.log(foo1()); console.log("foo2 returns:"); console.log(foo2()); |
將產生:
1 2 3 4 |
foo1 returns: Object {bar: "hello"} foo2 returns: undefined |
這不僅是令人驚訝,而且特別讓人困惑的是, foo2()
返回undefined卻沒有任何錯誤丟擲。
原因與這樣一個事實有關,即分號在JavaScript中是一個可選項(儘管省略它們通常是非常糟糕的形式)。其結果就是,當碰到 foo2()
中包含 return
語句的程式碼行(程式碼行上沒有其他任何程式碼),分號會立即自動插入到返回語句之後。
也不會丟擲錯誤,因為程式碼的其餘部分是完全有效的,即使它沒有得到呼叫或做任何事情(相當於它就是是一個未使用的程式碼塊,定義了等同於字串 "hello"
的屬性 bar
)。
這種行為也支援放置左括號於JavaScript程式碼行的末尾,而不是新程式碼行開頭的約定。正如這裡所示,這不僅僅只是JavaScript中的一個風格偏好。
7. NaN
是什麼?它的型別是什麼?你如何可靠地測試一個值是否等於 NaN
?
NaN
屬性代表一個“不是數字”的值。這個特殊的值是因為運算不能執行而導致的,不能執行的原因要麼是因為其中的運算物件之一非數字(例如, "abc" / 4
),要麼是因為運算的結果非數字(例如,除數為零)。
雖然這看上去很簡單,但 NaN
有一些令人驚訝的特點,如果你不知道它們的話,可能會導致令人頭痛的bug。
首先,雖然 NaN
意味著“不是數字”,但是它的型別,不管你信不信,是 Number
:
1 |
console.log(typeof NaN === "number"); // logs "true" |
此外, NaN
和任何東西比較——甚至是它自己本身!——結果是false:
1 |
console.log(NaN === NaN); // logs "false" |
一種半可靠的方法來測試一個數字是否等於 NaN,是使用內建函式 isNaN()
,但即使使用 isNaN()
依然並非是一個完美的解決方案。
一個更好的解決辦法是使用 value !== value
,如果值等於NaN,只會產生true。另外,ES6提供了一個新的 Number.isNaN()
函式,這是一個不同的函式,並且比老的全域性 isNaN()
函式更可靠。
8.下列程式碼將輸出什麼?並解釋原因。
1 2 |
console.log(0.1 + 0.2); console.log(0.1 + 0.2 == 0.3); |
一個稍微有點程式設計基礎的回答是:“你不能確定。可能會輸出“0.3”和“true”,也可能不會。JavaScript中的數字和浮點精度的處理相同,因此,可能不會總是產生預期的結果。“
以上所提供的例子就是一個演示了這個問題的典型例子。但出人意料的是,它會輸出:
1 2 |
0.30000000000000004 false |
9.討論寫函式 isInteger(x)
的可能方法,用於確定x是否是整數。
這可能聽起來是小菜一碟,但事實上,這很瑣碎,因為ECMAScript 6引入了一個新的正以此為目的 Number.isInteger()
函式。然而,之前的ECMAScript 6,會更復雜一點,因為沒有提供類似的 Number.isInteger()
方法。
問題是,在ECMAScript規格說明中,整數只概念上存在:即,數字值總是儲存為浮點值。
考慮到這一點,最簡單又最乾淨的ECMAScript6之前的解決方法(同時也非常穩健地返回 false
,即使一個非數字的值,如字串或 null
,被傳遞給函式)如下:
1 |
function isInteger(x) { return (x^0) === x; } |
下面的解決方法也是可行的,雖然不如上面那個方法優雅:
1 |
function isInteger(x) { return Math.round(x) === x; } |
請注意 Math.ceil()
和 Math.floor()
在上面的實現中等同於 Math.round()
。
或:
1 |
function isInteger(x) { return (typeof x === 'number') && (x % 1 === 0); |
相當普遍的一個不正確的解決方案是:
1 |
function isInteger(x) { return parseInt(x, 10) === x; } |
雖然這個以 parseInt
函式為基礎的方法在 x
取許多值時都能工作良好,但一旦 x
取值相當大的時候,就會無法正常工作。問題在於 parseInt()
在解析數字之前強制其第一個引數到字串。因此,一旦數目變得足夠大,它的字串就會表達為指數形式(例如, 1e+21
)。因此,parseInt()
函式就會去解析 1e+21
,但當到達 e
字串的時候,就會停止解析,因此只會返回值 1
。注意:
1 2 3 4 5 6 7 8 |
> String(1000000000000000000000) '1e+21' > parseInt(1000000000000000000000, 10) 1 > parseInt(1000000000000000000000, 10) === 1000000000000000000000 false |
10.下列程式碼行1-4如何排序,使之能夠在執行程式碼時輸出到控制檯? 為什麼?
1 2 3 4 5 6 |
(function() { console.log(1); setTimeout(function(){console.log(2)}, 1000); setTimeout(function(){console.log(3)}, 0); console.log(4); })(); |
序號如下:
1 2 3 4 |
1 4 3 2 |
讓我們先來解釋比較明顯而易見的那部分:
1
和4
之所以放在前面,是因為它們是通過簡單呼叫console.log()
而沒有任何延遲輸出的2
之所以放在3
的後面,是因為2
是延遲了1000毫秒(即,1秒)之後輸出的,而3
是延遲了0毫秒之後輸出的。
好的。但是,既然 3
是0毫秒延遲之後輸出的,那麼是否意味著它是立即輸出的呢?如果是的話,那麼它是不是應該在 4
之前輸出,既然 4
是在第二行輸出的?
要回答這個問題,你需要正確理解JavaScript的事件和時間設定。
瀏覽器有一個事件迴圈,會檢查事件佇列和處理未完成的事件。例如,如果時間發生在後臺(例如,指令碼的 onload
事件)時,瀏覽器正忙(例如,處理一個 onclick
),那麼事件會新增到佇列中。當onclick處理程式完成後,檢查佇列,然後處理該事件(例如,執行 onload
指令碼)。
同樣的, setTimeout()
也會把其引用的函式的執行放到事件佇列中,如果瀏覽器正忙的話。
當setTimeout()
的第二個引數為0的時候,它的意思是“儘快”執行指定的函式。具體而言,函式的執行會放置在事件佇列的下一個計時器開始。但是請注意,這不是立即執行:函式不會被執行除非下一個計時器開始。這就是為什麼在上述的例子中,呼叫 console.log(4)
發生在呼叫 console.log(3)
之前(因為呼叫 console.log(3)
是通過setTimeout被呼叫的,因此會稍微延遲)。
11.寫一個簡單的函式(少於80個字元),要求返回一個布林值指明字串是否為迴文結構。
下面這個函式在 str
是迴文結構的時候返回true,否則,返回false。
1 2 3 4 |
function isPalindrome(str) { str = str.replace(/W/g, '').toLowerCase(); return (str == str.split('').reverse().join('')); } |
例如:
1 2 3 |
console.log(isPalindrome("level")); // logs 'true' console.log(isPalindrome("levels")); // logs 'false' console.log(isPalindrome("A car, a man, a maraca")); // logs 'true' |
12.寫一個 sum
方法,在使用下面任一語法呼叫時,都可以正常工作。
1 2 |
console.log(sum(2,3)); // Outputs 5 console.log(sum(2)(3)); // Outputs 5 |
(至少)有兩種方法可以做到:
方法1
1 2 3 4 5 6 7 |
function sum(x) { if (arguments.length == 2) { return arguments[0] + arguments[1]; } else { return function(y) { return x + y; }; } } |
在JavaScript中,函式可以提供到 arguments
物件的訪問,arguments
物件提供傳遞到函式的實際引數的訪問。這使我們能夠使用 length
屬性來確定在執行時傳遞給函式的引數數量。
如果傳遞兩個引數,那麼只需加在一起,並返回。
否則,我們假設它被以 sum(2)(3)
這樣的形式呼叫,所以我們返回一個匿名函式,這個匿名函式合併了傳遞到 sum()
的引數和傳遞給匿名函式的引數。
方法2
1 2 3 4 5 6 7 |
function sum(x, y) { if (y !== undefined) { return x + y; } else { return function(y) { return x + y; }; } } |
當呼叫一個函式的時候,JavaScript不要求引數的數目匹配函式定義中的引數數量。如果傳遞的引數數量大於函式定義中引數數量,那麼多餘引數將簡單地被忽略。另一方面,如果傳遞的引數數量小於函式定義中的引數數量,那麼缺少的引數在函式中被引用時將會給一個 undefined
值。所以,在上面的例子中,簡單地檢查第2個引數是否未定義,就可以相應地確定函式被呼叫以及進行的方式。
13.請看下面的程式碼片段:
1 2 3 4 5 6 |
for (var i = 0; i < 5; i++) { var btn = document.createElement('button'); btn.appendChild(document.createTextNode('Button ' + i)); btn.addEventListener('click', function(){ console.log(i); }); document.body.appendChild(btn); } |
(a)當使用者點選“Button 4”的時候會輸出什麼到控制檯,為什麼?(b)提供一個或多個備用的可按預期工作的實現方案。
(a)無論使用者點選什麼按鈕,數字5將總會輸出到控制檯。這是因為,當 onclick
方法被呼叫(對於任何按鈕)的時候, for
迴圈已經結束,變數 i
已經獲得了5的值。(面試者如果能夠談一談有關如何執行上下文,可變物件,啟用物件和內部“範圍”屬性貢有助於閉包行為,則可以加分)。
(b)要讓程式碼工作的關鍵是,通過傳遞到一個新建立的函式物件,在每次傳遞通過 for
迴圈時,捕捉到 i
值。下面是三種可能實現的方法:
1 2 3 4 5 6 7 8 |
for (var i = 0; i < 5; i++) { var btn = document.createElement('button'); btn.appendChild(document.createTextNode('Button ' + i)); btn.addEventListener('click', (function(i) { return function() { console.log(i); }; })(i)); document.body.appendChild(btn); } |
或者,你可以封裝全部呼叫到在新匿名函式中的 btn.addEventListener
:
1 2 3 4 5 6 7 8 |
for (var i = 0; i < 5; i++) { var btn = document.createElement('button'); btn.appendChild(document.createTextNode('Button ' + i)); (function (i) { btn.addEventListener('click', function() { console.log(i); }); })(i); document.body.appendChild(btn); } |
也可以呼叫陣列物件的本地 forEach
方法來替代 for
迴圈:
1 2 3 4 5 6 |
['a', 'b', 'c', 'd', 'e'].forEach(function (value, i) { var btn = document.createElement('button'); btn.appendChild(document.createTextNode('Button ' + i)); btn.addEventListener('click', function() { console.log(i); }); document.body.appendChild(btn); }); |
14.下面的程式碼將輸出什麼到控制檯,為什麼?
1 2 3 4 5 6 |
var arr1 = "john".split(''); var arr2 = arr1.reverse(); var arr3 = "jones".split(''); arr2.push(arr3); console.log("array 1: length=" + arr1.length + " last=" + arr1.slice(-1)); console.log("array 2: length=" + arr2.length + " last=" + arr2.slice(-1)); |
輸出結果是:
1 2 |
"array 1: length=5 last=j,o,n,e,s" "array 2: length=5 last=j,o,n,e,s" |
arr1
和 arr2
在上述程式碼執行之後,兩者相同了,原因是:
- 呼叫陣列物件的
reverse()
方法並不只返回反順序的陣列,它也反轉了陣列本身的順序(即,在這種情況下,指的是arr1
)。 -
reverse()
方法返回一個到陣列本身的引用(在這種情況下即,arr1
)。其結果為,arr2
僅僅是一個到arr1
的引用(而不是副本)。因此,當對arr2
做了任何事情(即當我們呼叫arr2.push(arr3);
)時,arr1
也會受到影響,因為arr1
和arr2
引用的是同一個物件。
這裡有幾個側面點有時候會讓你在回答這個問題時,陰溝裡翻船:
傳遞陣列到另一個陣列的 push()
方法會讓整個陣列作為單個元素對映到陣列的末端。其結果是,語句 arr2.push(arr3);
在其整體中新增 arr3
作為一個單一的元素到 arr2
的末端(也就是說,它並沒有連線兩個陣列,連線陣列是 concat()
方法的目的)。
和Python一樣,JavaScript標榜陣列方法呼叫中的負數下標,例如 slice()
可作為引用陣列末尾元素的方法:例如,-1下標表示陣列中的最後一個元素,等等。
15.下面的程式碼將輸出什麼到控制檯,為什麼?
1 2 3 4 5 6 |
console.log(1 + "2" + "2"); console.log(1 + +"2" + "2"); console.log(1 + -"1" + "2"); console.log(+"1" + "1" + "2"); console.log( "A" - "B" + "2"); console.log( "A" - "B" + 2); |
上面的程式碼將輸出以下內容到控制檯:
1 2 3 4 5 6 |
"122" "32" "02" "112" "NaN2" NaN |
原因是…
這裡的根本問題是,JavaScript(ECMAScript)是一種弱型別語言,它可對值進行自動型別轉換,以適應正在執行的操作。讓我們通過上面的例子來說明這是如何做到的。
例1:1 + "2" + "2"
輸出:"122"
說明: 1 + "2"
是執行的第一個操作。由於其中一個運算物件("2"
)是字串,JavaScript會假設它需要執行字串連線,因此,會將 1
的型別轉換為 "1"
, 1 + "2"
結果就是 "12"
。然後, "12" + "2"
就是 "122"
。
例2: 1 + +"2" + "2"
輸出: "32"
說明:根據運算的順序,要執行的第一個運算是 +"2"
(第一個 "2"
前面的額外 +
被視為一元運算子)。因此,JavaScript將 "2"
的型別轉換為數字,然後應用一元 +
號(即,將其視為一個正數)。其結果是,接下來的運算就是 1 + 2
,這當然是 3
。然後我們需要在一個數字和一個字串之間進行運算(即, 3
和 "2"
),同樣的,JavaScript會將數值型別轉換為字串,並執行字串的連線,產生 "32"
。
例3: 1 + -"1" + "2"
輸出: "02"
說明:這裡的解釋和前一個例子相同,除了此處的一元運算子是 -
而不是 +
。先是 "1"
變為 1
,然後當應用 -
時又變為了 -1
,然後將其與 1
相加,結果為 0
,再將其轉換為字串,連線最後的 "2"
運算物件,得到 "02"
。
例4: +"1" + "1" + "2"
輸出: "112"
說明:雖然第一個運算物件 "1"
因為字首的一元 +
運算子型別轉換為數值,但又立即轉換回字串,當連線到第二個運算物件 "1"
的時候,然後又和最後的運算物件"2"
連線,產生了字串 "112"
。
例5: "A" - "B" + "2"
輸出: "NaN2"
說明:由於運算子 -
不能被應用於字串,並且 "A"
和 "B"
都不能轉換成數值,因此,"A" - "B"
的結果是 NaN
,然後再和字串 "2"
連線,得到 "NaN2"
。
例6: "A" - "B" + 2
輸出: NaN
說明:參見前一個例子, "A" - "B"
結果為 NaN
。但是,應用任何運算子到NaN與其他任何的數字運算物件,結果仍然是 NaN
。
16.下面的遞迴程式碼在陣列列表偏大的情況下會導致堆疊溢位。在保留遞迴模式的基礎上,你怎麼解決這個問題?
1 2 3 4 5 6 7 8 9 10 |
var list = readHugeList(); var nextListItem = function() { var item = list.pop(); if (item) { // process the list item... nextListItem(); } }; |
潛在的堆疊溢位可以通過修改nextListItem
函式避免:
1 2 3 4 5 6 7 8 9 10 |
var list = readHugeList(); var nextListItem = function() { var item = list.pop(); if (item) { // process the list item... setTimeout( nextListItem, 0); } }; |
堆疊溢位之所以會被消除,是因為事件迴圈操縱了遞迴,而不是呼叫堆疊。當 nextListItem
執行時,如果 item
不為空,timeout函式(nextListItem
)就會被推到事件佇列,該函式退出,因此就清空呼叫堆疊。當事件佇列執行其timeout事件,且進行到下一個 item
時,定時器被設定為再次呼叫 nextListItem
。因此,該方法從頭到尾都沒有直接的遞迴呼叫,所以無論迭代次數的多少,呼叫堆疊保持清空的狀態。
17.JavaScript中的“閉包”是什麼?請舉一個例子。
閉包是一個可以訪問外部(封閉)函式作用域鏈中的變數的內部函式。閉包可以訪問三種範圍中的變數:這三個範圍具體為:(1)自己範圍內的變數,(2)封閉函式範圍內的變數,以及(3)全域性變數。
下面是一個簡單的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var globalVar = "xyz"; (function outerFunc(outerArg) { var outerVar = 'a'; (function innerFunc(innerArg) { var innerVar = 'b'; console.log( "outerArg = " + outerArg + "n" + "innerArg = " + innerArg + "n" + "outerVar = " + outerVar + "n" + "innerVar = " + innerVar + "n" + "globalVar = " + globalVar); })(456); })(123); |
在上面的例子中,來自於 innerFunc
, outerFunc
和全域性名稱空間的變數都在 innerFunc
的範圍內。因此,上面的程式碼將輸出如下:
1 2 3 4 5 |
outerArg = 123 innerArg = 456 outerVar = a innerVar = b globalVar = xyz |
18.下面的程式碼將輸出什麼:
1 2 3 |
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, i * 1000 ); } |
解釋你的答案。閉包在這裡能起什麼作用?
上面的程式碼不會按預期顯示值0,1,2,3,和4,而是會顯示5,5,5,5,和5。
原因是,在迴圈中執行的每個函式將整個迴圈完成之後被執行,因此,將會引用儲存在 i
中的最後一個值,那就是5。
閉包可以通過為每次迭代建立一個唯一的範圍,儲存範圍內變數的每個唯一的值,來防止這個問題,如下:
1 2 3 4 5 |
for (var i = 0; i < 5; i++) { (function(x) { setTimeout(function() { console.log(x); }, x * 1000 ); })(i); } |
這就會按預期輸出0,1,2,3,和4到控制檯。
19.以下程式碼行將輸出什麼到控制檯?
1 2 3 4 |
console.log("0 || 1 = "+(0 || 1)); console.log("1 || 2 = "+(1 || 2)); console.log("0 && 1 = "+(0 && 1)); console.log("1 && 2 = "+(1 && 2)); |
並解釋。
該程式碼將輸出:
1 2 3 4 |
0 || 1 = 1 1 || 2 = 1 0 && 1 = 0 1 && 2 = 2 |
在JavaScript中, ||
和 &&
都是邏輯運算子,用於在從左至右計算時,返回第一個可完全確定的“邏輯值”。
或( ||
)運算子。在形如 X||Y
的表示式中,首先計算X
並將其解釋執行為一個布林值。如果這個布林值true
,那麼返回true
(1),不再計算 Y
,因為“或”的條件已經滿足。如果這個布林值為false
,那麼我們仍然不能知道 X||Y
是真是假,直到我們計算 Y
,並且也把它解釋執行為一個布林值。
因此, 0 || 1
的計算結果為true(1),同理計算1 || 2
。
與( &&
)運算子。在形如 X&&Y
的表示式中,首先計算 X
並將其解釋執行為一個布林值。如果這個布林值為 false
,那麼返回 false
(0),不再計算 Y
,因為“與”的條件已經失敗。如果這個布林值為true
,但是,我們仍然不知道 X&&Y
是真是假,直到我們去計算 Y
,並且也把它解釋執行為一個布林值。
不過,關於 &&
運算子有趣的地方在於,當一個表示式計算為“true”的時候,那麼就返回表示式本身。這很好,雖然它在邏輯表示式方面計算為“真”,但如果你希望的話也可用於返回該值。這就解釋了為什麼,有些令人奇怪的是, 1 && 2
返回 2
(而不是你以為的可能返回 true
或 1
)。
20.執行下面的程式碼時將輸出什麼?請解釋。
1 2 |
console.log(false == '0') console.log(false === '0') |
程式碼將輸出:
1 2 |
true false |
在JavaScript中,有兩種等式運算子。三個等於運算子 ===
的作用類似傳統的等於運算子:如果兩側的表示式有著相同的型別和相同的值,那麼計算結果為true。而雙等於運算子,會只強制比較它們的值。因此,總體上而言,使用 ===
而不是 ==
的做法更好。 !==
vs !=
亦是同理。
21.以下程式碼將輸出什麼?並解釋你的答案。
1 2 3 4 5 6 7 8 |
var a={}, b={key:'b'}, c={key:'c'}; a[b]=123; a[c]=456; console.log(a[b]); |
這段程式碼將輸出 456
(而不是 123
)。
原因為:當設定物件屬性時,JavaScript會暗中字串化引數值。在這種情況下,由於 b
和 c
都是物件,因此它們都將被轉換為"[object Object]"
。結果就是, a[b]
和a[c]
均相當於a["[object Object]"]
,並可以互換使用。因此,設定或引用 a[c]
和設定或引用 a[b]
完全相同。
22.以下程式碼行將輸出什麼到控制檯?
1 |
console.log((function f(n){return ((n > 1) ? n * f(n-1) : n)})(10)); |
並解釋你的答案。
程式碼將輸出10!的值(即10!或3628800)。
原因是:
命名函式 f()
遞迴地呼叫本身,當呼叫 f(1)
的時候,只簡單地返回1
。下面就是它的呼叫過程:
1 2 3 4 5 6 7 8 9 10 |
f(1): returns n, which is 1 f(2): returns 2 * f(1), which is 2 f(3): returns 3 * f(2), which is 6 f(4): returns 4 * f(3), which is 24 f(5): returns 5 * f(4), which is 120 f(6): returns 6 * f(5), which is 720 f(7): returns 7 * f(6), which is 5040 f(8): returns 8 * f(7), which is 40320 f(9): returns 9 * f(8), which is 362880 f(10): returns 10 * f(9), which is 3628800 |
23.請看下面的程式碼段。控制檯將輸出什麼,為什麼?
1 2 3 4 5 |
(function(x) { return (function(y) { console.log(x); })(2) })(1); |
控制檯將輸出 1
,即使從來沒有在函式內部設定過x
的值。原因是:
正如我們在JavaScript招聘指南中解釋過的那樣,閉包是一個函式,連同在閉包建立的時候,其範圍內的所有變數或函式一起。在JavaScript中,閉包是作為一個“內部函式”實施的:即,另一個函式主體內定義的函式。閉包的一個重要特徵是,內部函式仍然有權訪問外部函式的變數。
因此,在本例中,由於 x
未在函式內部中定義,因此在外部函式範圍中搜尋定義的變數 x
,且被發現具有1
的值。
24.下面的程式碼將輸出什麼到控制檯,為什麼:
1 2 3 4 5 6 7 8 9 10 11 |
var hero = { _name: 'John Doe', getSecretIdentity: function (){ return this._name; } }; var stoleSecretIdentity = hero.getSecretIdentity; console.log(stoleSecretIdentity()); console.log(hero.getSecretIdentity()); |
程式碼有什麼問題,以及應該如何修復。
程式碼將輸出:
1 2 |
undefined John Doe |
第一個 console.log
之所以輸出 undefined
,是因為我們正在從 hero
物件提取方法,所以呼叫了全域性上下文中(即視窗物件)的 stoleSecretIdentity()
,而在此全域性上下文中, _name
屬性不存在。
其中一種修復stoleSecretIdentity()
函式的方法如下:
1 |
var stoleSecretIdentity = hero.getSecretIdentity.bind(hero); |
25.建立一個給定頁面上的一個DOM元素,就會去訪問元素本身及其所有子元素(不只是它的直接子元素)的函式。對於每個被訪問的元素,函式應該傳遞元素到提供的回撥函式。
此函式的引數為:
- DOM元素
- 回撥函式(將DOM元素作為其引數)
訪問樹(DOM)的所有元素是經典的深度優先搜尋演算法應用。下面是一個示範的解決方案:
1 2 3 4 5 6 7 |
function Traverse(p_element,p_callback) { p_callback(p_element); var list = p_element.children; for (var i = 0; i < list.length; i++) { Traverse(list[i],p_callback); // recursive call } } |