原網址:37 Essential JavaScript Interview Questions
以下爲我對這37個題目的翻譯和解答,其中小部分題目的解答是我認爲官方解答的很合適,也無需更爲深入的挖掘,會直接翻譯官方的回答;大部分題目的解答都是我對題中涉及到的知識點更深入的挖掘做出的解釋。
1.當你使用typeof bar === 'object'
來確定 bar
是否是 object 時,這其中存在潛在問題是什麼?如何規避這些問題?
通常情況下,使用 typeof 來判斷某個變量的類型,是沒有什麼問題的,但是如果你有一些特殊的需求,可能就會存在潛在問題了。
我們舉個例子,你有個變量 a
,需要做類型校驗,如果是 object
類型就 true
,程序繼續往下走,此時,我們希望出現的結果是什麼?是這個 a
變量不爲空,且有價值數據,這個數據是一個 object
類型的對象,我們希望拿到這個對象數據來進行某些處理。
那麼此時由於一些未知的原因,你沒有拿到預期的數據,你拿到了一個 null
,此時你當然是希望你的判斷條件不會命中它,因爲一旦命中 null
的話,我們處理數據的邏輯部分很可能就會出現些不可預知的 bug 。
但是事實呢?很不幸的告訴你,它通過了你的判斷條件,typeof null === 'object'
會打印出 true
這一結果。
我們在這並不想深究爲什麼在javascript中null
會是一個object對象,正如同我們並不想深究NaN
爲什麼是一個 number
類型,而 undeifined
是一個 undeifined
類型一樣,我們希望可以讓你知道如何去分辨和規避,而不是告訴你爲什麼爲什麼 null
是一個 object
。
言歸正傳,在上述示例中,還有一個特殊的情況,比如說,Array
。typeof [] === 'object'
這句代碼在 javascript 中是成立的,如果你想真正區分 object
和 Array
,那麼你可以像下面這樣修改你的命中條件:
if(Object.prototype.toString.call(a) === '[object Array]'){
// do something
}
或者還可以這樣:
if(a != null && a.constructor === Object){
// do something
}
當然,用ES5的 isArray()
也是可以的:
if(Array.isArray(a)){
// do something
}
要注意,a.constructor
並不是一個通用的解決方案,比如有以下代碼:
const b = null;
b.constructor === Object // Uncaught TypeError: Cannot read property 'constructor' of null
const b = undefined;
b.constructor === Undeifined // Uncaught TypeError: Cannot read property 'constructor' of undefined
const b = NaN;
b.constructor === Number; // logs true
2.以下的代碼會輸出什麼?爲什麼呢?
(function(){
var a = b = 3;
})();
console.log("a defined? " + (typeof a !== 'undefined'));
console.log("b defined? " + (typeof b !== 'undefined'));
這個問題有趣的地方在於你對 javascript 的關鍵字聲明是否熟悉。你也許認爲以下的輸出是對的:
"a defined? " false
"b defined? " false
但事實上很多人把 var a=b=3
當成了以下這種形式:
var a = 3;
var b = 3;
但是事實上,var a=b=3
應該是這樣的:
b = 3;
var a = b;
如果你使用了嚴格模式(use strict
),就沒有這個困擾了,因爲在運行時會報以下錯誤:ReferenceError: b is not defined
.
所以,正確的輸出應該如下:
"a defined? " false
"b defined? " true
3.以下的代碼塊會輸出什麼內容?爲什麼?
var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log("outer func1: this.foo = " + this.foo);
console.log("outer func2: self.foo = " + self.foo);
(function() {
console.log("inner func1: this.foo = " + this.foo);
console.log("inner func2: self.foo = " + self.foo);
}());
}
};
myObject.func();
這是一個典型的 JavaScript
指向的問題,對此有疑惑的可以去看看我的一篇專門解釋 this 指向的文章:javascript this探究
這兒我就簡單解釋下這其中的原理,首先正確的輸出結果應該是如下:
outer func1: this.foo = 'bar',
outer func2: this.foo = 'bar'
inner func1: this.foo = undefined,
inner func2: self.foo = 'bar'
要想找到 this 的指向,只要找到這個 this 的(直接調用者)上一級調用者就ok了。在上述代碼中,這個this(self)出在 func 函數內,也就是說,此 this
與 func
的地位是相等的,而 func
又由 myObject
這個對象調用,所以 outer 中 this 指向的就是 myObject
對象。
而在 inner
函數中的 this
是與這個匿名函數同級的,而這個匿名函數被 func
函數調用,它的上一級調用者就是 func
函數,所以this自然就是 undefined
了,而此匿名函數又處在 func
函數的作用域範圍內,所以調用 self
變量時還是能拿到 bar
值;
4.將 JavaScript 源文件的整個內容包在閉包中的意義和原因是什麼?
這是一種越來越普遍的做法,被許多流行的 JavaScript
庫(jQuery,Node.js等)採用。這種技術圍繞文件的整個內容創建一個閉包,最重要的是,它可以創建一個私有命名空間,從而有助於避免不同 JavaScript
模塊和庫之間潛在的名稱衝突。
該技術的另一個特徵是允許使用更易於引用(可能更短)的全局變量的別名。例如,在 jQuery 插件中經常使用它。如下所示:
(function($) {
/* jQuery plugin code referencing $ */
})(jQuery);
5.use strict
在 JavaScript 源文件的開頭包含什麼是重要的,有什麼好處?
簡單來說,use strict
是一種在代碼運行時自動對 JavaScript
代碼實施更嚴格的解析和錯誤處理的方法。在未使用 use strict
時會被忽略或會以靜默方式失敗的代碼,錯誤在使用了 use strict
現在將生成錯誤或拋出異常,光從這個角度來說,這就是一個很好的辦法。
嚴格模式的一些主要好處包括:
1.使調試更容易。
否則將被忽略或將以靜默方式失敗的代碼錯誤現在將生成錯誤或拋出異常,提前警告您代碼中的問題並將您更快地引導到其源代碼。
防止偶然的全局變量。如果沒有嚴格模式,則爲未聲明的變量賦值會自動創建具有該名稱的全局變量。這是 JavaScript
中最常見的錯誤之一。在嚴格模式下,嘗試這樣做會引發錯誤。
2.消除this脅迫。
如果沒有嚴格模式,則 this
對 null
或 undefined
值的引用會自動強制轉換爲全局。這可能導致許多頭屑和拔出你的頭髮類型的錯誤。在嚴格模式下,引用 this
, null
或 undefined
值會引發錯誤。
3.禁止重複的參數值。 嚴格模式在檢測到函數的重複命名參數時會拋出錯誤(例如,function foo(val1, val2, val1){})
,從而捕獲代碼中幾乎可以肯定的錯誤,否則您可能會浪費大量時間進行跟蹤。
注意:以前(在 ECMAScript 5中)嚴格模式將禁止重複的屬性名稱(例如var object = {foo: "bar", foo: "baz"};)
,但從 ECMAScript 2015 開始,情況不再如此。
4.使eval()更安全。
eval() 在嚴格模式和非嚴格模式下的行爲 方式存在一些差異。最重要的是,在嚴格模式下,聲明內部 eval() 聲明的變量和函數不會在包含範圍中創建(它們是在非嚴格模式的包含範圍中創建的,這也可能是常見的問題來源)。
5.無效使用時會引發錯誤delete。
delete 操作者(用於從對象中刪除屬性)不能在對象的非配置的屬性來使用。當嘗試刪除不可配置的屬性時,非嚴格代碼將無提示失敗,而嚴格模式將在這種情況下拋出錯誤。
6.以下兩個函數都會返回相同的內容嗎?爲什麼?
function foo1()
{
return {
bar: "hello"
};
}
function foo2()
{
return
{
bar: "hello"
};
}
我們在執行這兩個函數的時候,會發現一件讓人驚訝的事:
console.log("foo1 returns:");
console.log(foo1());
console.log("foo2 returns:");
console.log(foo2());
會有以下結果:
foo1 returns:
Object {bar: "hello"}
'foo2 returns:'
undefined
主要原因就是 return
後面的對象符 }
換行了。瀏覽器 JavaScript
引擎在解析時,會自動在換行的部分插入分號;,所以上述代碼中的 foo2()
函數就如同下面:
function foo2()
{
return;
}
那返回的自然就是一個 undefined
了。
7.什麼是 NaN?如何用一個可靠的方法來判斷它?
NaN
是一個全局屬性,表示不是一個數字( Not a Number )。
通常來說,NaN
很少在編碼中出現,它一般都是被某個方法計算錯誤時作爲返回值給我們。
NaN
有着一些很奇怪的特性:
1.NaN 和任何值都不相等
console.log(NaN === NaN) // logs false
console.log(NaN === 1) // logs false
console.log(NaN === '1') // logs false
2.NaN 的數據類型爲 number
console.log(typeof NaN) // logs number
JavaScript
提供了一個內置的函數 isNaN()
來判斷 NaN
,它的作用機制是檢查一個值是否能被 Number()
成功轉換。如果能轉換成功,就返回 false
,否則返回 true
,事實上,它並不是一個理想的判斷函數:
console.log(isNaN(NaN)) // logs true 不能轉換
console.log(isNaN('123')) // logs false 能轉換
console.log(isNaN('abc')) // logs true 不能轉換
console.log(isNaN('123.45abc')) // logs true 不能轉換
顯然,它不能區分 NaN
和其他不能被轉換的類型。
在 ES5
之前,利用 NaN
與其自身的絕對不相等性,以下方式能夠更可靠的進行 NaN
的驗證:
var isNaN = function (n){
return n !== n;
}
console.log(isNaN(1)) // logs false
console.log(isNaN('1.111')) // logs false
console.log(isNaN(NaN)) // logs true
ES5 之後我們可以使用 Number.isNaN()
來做判斷,更安全可靠。
8.下面的代碼會輸出什麼?爲什麼?
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 == 0.3);
我們先看結果:
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 == 0.3); // false
這個問題本質上是在講 JavaScript
在進行浮點數計算時精度丟失的問題。當然,不僅僅是 JavaScript
,所有遵循 IEEE 754
標準的編程語言都存在這個問題。
這是什麼原因導致的呢?就是十進制數在轉化成二進制數時產生的精度丟失。我稍微演示下精度丟失的過程:
準備工作:想要理解精度是怎麼丟失的,你先得理解十進制數是怎麼轉化爲二進制數的。
十進制數轉化爲二進制數主要分爲兩步:
1.整數部分按位取餘,然後倒過來。比如:
//十進制 5 轉化爲二進制(向下取整)
5/2 = 2 --- 餘1
2/2 = 1 --- 餘0
1/2 = 0 --- 餘1
所以 5 的二進制數據是 0101。
2.小數部分按位乘2取整,得到積後取積的小數部分,然後再把這個積的小數部分乘2,用積取整,直到積的小數部分爲0。比如:
//十進制 0.625 轉化爲二進制(按積取整)
0.625 * 2 = 1.25 --- 取 1
0.25 * 2 = 0.5 --- 取 0
0.5 * 2 = 1 --- 取 1
所以 0.625 的二進制數據是 0.101。
OK,準備工作做完了,我們來看看 0.1 的二進制數據是怎麼轉化的:
0.1 * 2 = 0.2 --- 0
0.2 * 2 = 0.4 --- 0
0.4 * 2 = 0.8 --- 0
0.8 * 2 = 1.6 --- 1
0.6 * 2 = 1.2 --- 1
0.2 * 2 = 0.4 --- 0
0.4 * 2 = 0.8 --- 0
0.8 * 2 = 1.6 --- 1
0.6 * 2 = 1.2 --- 1
...
看到這想必各位看官已經有所領悟了,若是碰到這種無限循環的數據,肯定是隻能通過截取部分有效位來處理,所以在截取的過程中自然就會產生精度丟失。
0.1 -> 0.000110011......0011
0.2 -> 0.00110011......0011
0.1 + 0.2 -> 0.010011001100110011001100110011001100110011001100110100
ok,我們再將這個被截取的二進制數按照二次冪的原則轉化爲十進制數是多少呢?
0.30000000000000004440892098500626
到此爲止,我們已經知道了0.30000000000000004
這個值是怎麼來的了。
那麼怎麼處理呢?
1.部署一個誤差檢查函數。
function areTheNumbersAlmostEqual(num1, num2) {
return Math.abs( num1 - num2 ) < Number.EPSILON;
}
console.log(areTheNumbersAlmostEqual(0.1 + 0.2, 0.3)); // true
Number.EPSILON
是 ES6
新增的一個極小的常量,它實際上是 JavaScript
能夠表示的最小精度。誤差如果小於這個值,就可以認爲已經沒有意義了,即不存在誤差了。
2.在進行浮點數計算時,先講浮點數轉化爲整型,計算後,在根據位數轉化回小數。
9.ECMAscript 6
中的 Number.isInteger()
(用來確定是否是整數) 在 ES5
中可以怎麼實現?
在 ECMAscript 6
中,我們可以很方便的使用Number.isInteger()
來判斷一個數是否爲整數。但是在 ES6
以前,這是一件挺麻煩的事。
所以,一個最簡單幹淨的解決辦法是以下這種:
var isInteger = function(x) {
return (x ^ 0) === x;
}
我來簡單解釋下return (x ^ 0) === x
這句代碼的含義。首先,你得明白 JavaScript
中按位異或的概念(以 ^ 來表示):即兩個位數相等則爲 0,不等則爲 1。
舉個例子:
// 5 ^ 1 按位異或的過程:
5 -> 0101
1 -> 0001
5^1 -> 0100
5^1 = 4
我們再來看這句代碼:return (x ^ 0) === x
,我們可以從兩個方面來解釋,第一,如果這個數它本來就是整數,那麼會返回什麼?
5 -> 0101
0 -> 0000
5^0 -> 0101
5^0 = 5
很顯然我們得到結論,任何數和0按位異或時得到的結果都是它本身。
ok,那麼第二,當這個數不是整數呢?,我們看看會發生什麼:
5.1 ^ 0 = 5
2.11111 ^ 0 = 2
很奇怪不是嗎?按照之前的理論,應該是下面這種過程纔對啊:
5.1 -> 0101.0001100110011.......0011
0 -> 0000.0000000000000.......0000
5.1^0->0101.0001100110011.......0011
5.1^0 = 5.1
這樣纔對呀!但是事實上並非如此,問題就出在小數部分的異或上面,我們接着看一個例子:
0.1 ^ 0 = 0;
0.2222 ^ 0 = 0;
1/3 ^ 0 = 0;
0.1 ^ 1 = 1;
0.2222 ^ 1 = 1;
1/3 ^ 1 = 1;
發現沒有,在 JavaScript
中,任何小數與一個數(假定它爲 a)異或時,都等於這個數 a。
這是爲什麼呢?我們打開 ECMAScript5.1中文版
在 ***9.5 ToInt32:(32 位有符號整數)***中,有以下內容:
ToInt32 運算符將其在 -231 到 231-1 閉區間內的參數轉換爲 232 個整數值之一。此運算符功能如下所示:
對輸入參數調用 ToNumber。
1.如果 Result(1) 是 +0 ,-0,+∞,或 -∞,返回 +0。
2.計算 sign(Result(1)) * floor(abs(Result(1)))。
3.計算 Result(3) modulo 232 ;也就是說,數值類型的有限整數值 k 爲正,且小於 232 ,規模相對於 Result(3) 的數學值差異 ,232 是 k 的整數倍。
4.如果 Result(4) 是大於等於 231 的整數,返回 Result(4) - 232 ,否則返回 Result(4)。
關鍵就在第 2 條中的這句話:sign(Result(1)) * floor(abs(Result(1)))
中的floor(x)
函數。
這玩意是怎麼運作的呢?在ECMAScript5.1中文版中的 5.2 算法約定這一節中有明確的規定:
floor(x) = x−(x modulo 1).
什麼意思呢?**就是 floor(x) 等於 x 減去 x 模1。**正是這一步,將小數部分的異或去掉了。
到這裏,想必大家已經對上面那句簡單的return (x ^ 0) === x
有所領悟了。也正是因爲這一特性,我們也可以使用下面這種方式來實現判斷一個數是否爲整數
var isInteger = function(x) {
return Math.round(x) === x;
}
當然,你可以更奔放些,像下面這樣:
var isInteger = function(x) {
return (typeof x === 'number') && (x % 1 === 0);
}
但是,要注意的是,我們不可以用下面這種方式來處理:
var isInteger = function(x) {
return parseInt(x, 10) === x;
}
這是因爲 parseInt(string, radix)
與 Math.round(x)
不同的作用機制導致的。parseInt(string, radix)
會將它的第一個值轉化爲字符串類型備用,而這正是問題所在,當一個特別大的數字被傳入 parseInt
函數時,它會先將這個數轉化爲指數形式,就像下面這樣:
parseInt(1000000000000000000000, 10)
->
parseInt('1e+21', 10)
=
1
10.在執行以下代碼時,數字1-4將以什麼順序記錄到控制檯?爲什麼?
(function() {
console.log(1);
setTimeout(function(){console.log(2)}, 1000);
setTimeout(function(){console.log(3)}, 0);
console.log(4);
})();
首先先說答案:
1
4
3
2
然後說結論:之所以會出現這個情況,4比3先執行,是因爲要記住一點,定時器,都是異步執行的。JavaScript
對於異步執行的事件是有一個事件隊列的,這個隊列的執行優先級低於當前環境中代碼的執行優先級,所以纔會出現 4 比 3 先執行的情況。
11.編寫一個簡單的函數來判斷某個字符串是否爲迴文結構
首先,我和大家解釋下什麼是迴文結構。
按照維基百科的解釋,迴文結構就是將這個字符串的內容按相反的順序重新排列後,所得到的字符串和原來的一樣。
ok,瞭解了這一點之後,咱們就可以看看其實現了:
function isPalindrome(str) {
str = str.replace(/\W/g, '').toLowerCase();
return (str == str.split('').reverse().join(''));
}
console.log(isPalindrome("level")); // logs 'true'
console.log(isPalindrome("levels")); // logs 'false'
console.log(isPalindrome("A car, a man, a maraca")); // logs 'true'
當然,上面這種方式由於夾雜了切割,翻轉以及插入等各種操作,效率上會比較低,所以你還可以使用以下這種方式,從字符串頭部和尾部,逐步往中間檢測:
function isPalindrome(str) {
str = str.replace(/\W/g, '').toLowerCase();
for(var i = 0,j = str.length - 1; i < j; i++,j--){
if(str.charAt(i) !== str.charAt(j)){
return false;
}
}
return true;
}
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方法,使用下面的語法調用時將正常工作。
console.log(sum(2,3)); // Outputs 5
console.log(sum(2)(3)); // Outputs 5
至少有兩種方法可以做到這一點。
方法一:
function sum(x) {
if(arguments.length == 2){
return arguments[0] + arguments[1];
}else {
return function(y) {
return x + y;
}
}
}
方法二:
function sum(x, y) {
if(typeof y != 'undefined'){
return x + y;
}else {
return function(y) {
return x + y;
}
}
}
關於 arguments,我就不做過多的解釋了,如果你對這個參數還沒有概念,那麼你要加油了。
13.請觀察如下代碼:
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) 至少提供一個可按預期工作的替代方案?
很顯然,這是一個 JavaScrip
t 中經典的閉包問題。當你點擊 “Button 4” 時,只會打印出 5,因爲在你點擊的時候這個循環早已經結束了。所以纔會出現你無論你點擊哪一個 Button 都會顯示 5。
有關閉包的解釋和預期方案我不做過多解釋,我專門寫有一篇博客介紹 JavaScript
閉包以及怎麼處理它在循環中出現的問題。這是地址 JavaScript 閉包機制的詳解。
當然,你也可以去MDN上看關於閉包的解釋:MDN-閉包。
14.下面的代碼會輸出什麼內容到控制檯,爲什麼?
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));
先給出答案,會輸出以下內容:
"array 1: length= 5 last= j,o,n,e,s"
"array 2: length= 5 last= j,o,n,e,s"
接下來我再講講爲什麼。首先我先給出大家可能會疑惑的點:
arr2 = ['n', 'h', 'o', 'j', ['j', 'o', 'n', 'e', 's']]
,爲什麼arr1.slice(-1)
輸出的結果會是j,o,n,e,s
?- 爲什麼 arr1 會輸出和 arr2 一樣的結果?
ok,我們一個點一個點來講。首先說說第一點, arr.slice()
方法返回的會是一個新數組,而且它是淺拷貝。所以如果單獨看這句代碼:
console.log(arr2.slice(-1)) // logs [['j', 'o', 'n', 'e', 's']]
之所以會出現 last= j,o,n,e,s 這樣的結果,關鍵就在於 console.log
中的 " last=" + 的這個 + 號。
我們先來看一個例子:
1 + [] = "1"
1 + {} = "1[object object]"
1 + NaN = NaN
[] + {} = "[object object]"
在 JavaScript
中加法其實歸根到底還是 數字+數字 以及 字符串+字符串兩種模式,所以在一個加法運算中,無論是什麼類型的兩個數據相加,結果必然是 number 或者 string 類型中的一個。至於這加法其中的門門道道,有興趣的夥計可以看看我這篇博客:JavaScript 中神奇的加法。
再來說說第二點, 爲什麼 arr1
會輸出和 arr2
一樣的結果?如果你認真讀過 《JavaScript 高級程序設計(第三版)》
的話,你應該就知道爲什麼了。
翻開 JS高程 第70頁,有這麼一段話:
當一個變量向另一個變量複製引用類型的值時,同樣也會將儲存在變量對象中的複製一份放到爲新變量分配的空間中。不同的是,這個值的副本實際上是一個指針,而這個指針指向存儲在堆中的一個對象。複製操作結束後,兩個變量實際上將引用同一個對象。因此,改變一個變量,就會影響到另一個變量。
那麼什麼是引用類型的變量呢?很簡單,object
類型的都是引用類型變量,除了它,其他的形如 number, string, boolean, null, undefined
都是數值類型的變量。
現在再回到我們上面那個題目,原因就一目瞭然了對吧。那麼如果想避免這種情況怎麼辦?也很好辦,第一個思路是重新給變量分配堆中的內存空間;第二個思路是將原對象解構後再重組,指針自然也就不再指向原來的對象了。我們來看例子:
var arr1 = "john".split('');
var arr2 = [].concat(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));
會輸出以下內容:
array 1: length=4 last=j
array 2: length=5 last=j,o,n,e,s
當然,面對一些比較複雜的數據,你也可以嘗試直接遍歷解構來達到解除堆引用的目的。
15.下面的代碼會輸出什麼內容到控制檯中?爲什麼?
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);
這是一個典型的 JavaScript
加減法問題,它體現了這門語言在類型轉換和校驗上的特點。
我們先來說說答案:
"122"
"32"
"02"
"112"
"NaN2"
NaN
我簡單解釋下爲什麼會得到以下結果,如果你想了解其中的工作原理,建議你去看看我這篇文章,相信會給你帶來一些收穫::JavaScript 中神奇的加法。
首先來看 1 + "2" + "2"
,根據加法中的 從左到右原則 和 凡有一個字符串就將其他變量轉換爲字符串原則 這兩個原則,很容易得出 "122"
這個結果。
然後我們看看第2,3,4條,你會發現他們其實是類似的轉換規則。無非就是在某個字符串前面加了一個+
或者-
。但是當這個+
號只出現在單個變量的前面時,它就成一個二元運算符變成了一個一元運算符,其作用是將這個變量轉換成 number
類型,其功能與 Number()
類似。
So~,我們再來看第2,3,4條,無非就是先把+"2"
轉爲了2
,-"1"
轉成了-1
,然後再進行字符串拼接。這並不困難,夥計們可以想想下面這幾個會是什麼結果:
+null = ?
+undefined = ?
+true = ?
+[] = ?
+{} = ?
+function(){} = ?
最後我們看看倒數兩條,首先他們兩都有減法,而減法是會將兩個參與算術的變量都轉成 number
類型,"A"
轉成 number
類型自然就是 NaN
了。
16.如果數組列表太大,以下遞歸代碼將導致堆棧溢出。你如何在保留遞歸的前提下解決這個問題?
var list = readHugeList();
var nextListItem = function() {
var item = list.pop();
if (item) {
// process the list item...
nextListItem();
}
};
通過修改 nextListItem 函數可以避免潛在的堆棧溢出,如下所示:
var list = readHugeList();
var nextListItem = function() {
var item = list.pop();
if (item) {
// process the list item...
setTimeout( nextListItem, 0);
}
};
利用事件循環機制(Event Loop
)來處理遞歸而不是通過調用堆棧,因此消除了堆棧溢出。當 nextListItem
被調用時,如果 item
不爲空且不爲 undefined
,定時器會將(nextListItem
)加到任務隊列中並結束對該函數的調用,從而留下一個乾淨的調用堆棧。當任務隊列開始執行這個定時器裏的內容時,將會處理下一次的事件並再次設置一個定時器以調用 nextListItem
。因此,在沒有進行直接遞歸調用的情況下來處理該函數,無論遞歸的次數是多少,調用棧都保持乾淨不會發生溢出現象。
17.什麼是JavaScript中的“閉包”?舉個例子。
我就不舉例子也不解釋了,大家可以直接看官網的答案,如果覺得它解釋的不清晰的,可以看看我的這篇博客:JavaScript 閉包機制的詳解
18.以下代碼的輸出結果如何,解釋你的答案。如何使用閉包有助於此?
for (var i = 0; i < 5; i++) {
setTimeout(function() { console.log(i); }, i * 1000 );
}
這是一個典型的閉包,之前有好幾個類似的問題,它會輸以下答案:
5
5
5
5
5
原因就是循環中定時器裏的匿名函數形成了5個閉包,但是這5個閉包的執行上下文缺失同一個,因爲早在定時器開始執行前 for 循環就已經結束了。
解決辦法一般是三種:
- 用匿名執行函數把代碼塊包裹起來。
- 利用工廠函數。
- 利用 ES6 let 形成的塊級作用域。
下面是示例:
// 匿名執行函數
for (var i = 0; i < 5; i++) {
(function(i){setTimeout(function() { console.log(i); }, i * 1000 )})(i);
}
//工廠函數
function logCallBack(i){
return function(){
console.log(i);
}
}
for (var i = 0; i < 5; i++) {
setTimeout(logCallBack(i), i * 1000 );
}
//ES6 let
for (let i = 0; i < 5; i++) {
setTimeout(function() { console.log(i); }, i * 1000 );
}
19.以下代碼行輸出到控制檯的內容是什麼?請解釋你的答案。
console.log("0 || 1 = "+(0 || 1));
console.log("1 || 2 = "+(1 || 2));
console.log("0 && 1 = "+(0 && 1));
console.log("1 && 2 = "+(1 && 2));
這主要是考察 JavaScript
邏輯運算符的作用。先看結果:
0 || 1 = 1
1 || 2 = 1
0 && 1 = 0
1 && 2 = 2
再來講講這三個邏輯運算符的作用,咱們先從邏輯與 && 開始.
1.邏輯與 &&
- 兩邊條件都爲
true
時,結果才爲true
; - 如果有一個爲
false
,結果就爲false
; - 當第一個條件爲
false
時,就不再判斷後面的條件。
注意:當數值參與邏輯與運算時,結果爲 true,那麼會返回的會是第二個爲真的值;如果結果爲 false,返回的會是第一個爲假的值。
2.邏輯或 ||
- 只要有一個條件爲
true
時,結果就爲true
; - 當兩個條件都爲
false
時,結果才爲false
; - 當一個條件爲
true
時,後面的條件不再判斷。
注意:當數值參與邏輯或運算時,結果爲 true,會返回第一個爲真的值;如果結果爲 false,會返回第二個爲假的值。
3.邏輯非 !
- 當條件爲
false
時,結果爲true
;反之亦然。
20.執行以下代碼時輸出結果是什麼?請解釋爲什麼。
console.log(false == '0')
console.log(false === '0')
這個其實也是隱式轉換的問題。先說結果吧:
true
false
原因就是在 JavaScript
中 ==
運算符會進行隱式轉換,也就是說上面的 fasle
會被 Number()
變成 0
然後再被 toString()
變成 "0"
,所以爲 true
。
但是要注意的是,運算符 ===
是不會進行隱式轉換的,因爲它要對比你的變量類型。 false
爲boolean
,"0"
爲string
,所以爲false
。
21.以下代碼的輸出是什麼?請解釋你的答案。
var a={},
b={key:'b'},
c={key:'c'};
a[b]=123;
a[c]=456;
console.log(a[b]);
此代碼的輸出將是 456
(而不是123
)。
原因如下:在設置對象的屬性時,JavaScript
將隱式字符串化 key
值。在這種情況下,因爲 b 和 c 都是對象,它們將都被轉換成 "[object Object]"
。所以無論是 a[b]
還是 a[c]
其 key
值都相同。所以 a[c]=456
這一句代碼實際上是更新了key爲[object object]
的屬性值。
22.以下代碼將輸出上面內容到控制檯,並解釋你的答案。(本題無過多深究內容,答案爲官網答案)
console.log((function f(n){return ((n > 1) ? n * f(n-1) : n)})(10));
代碼將輸出10階乘的值(即10!或3,628,800)。
原因如下:
命名函數以 f()
遞歸方式調用自身,直到調用 f(1)
簡單返回爲止 1
。因此,這就是它的作用:
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.請考慮下面的代碼段。控制檯輸出是什麼以及爲什麼?
(function(x) {
return (function(y) {
console.log(x);
})(2)
})(1);
輸出將是1,即使 x
從未在內部函數中設置值。原因如下:
閉包是一個函數,以及創建閉包時在範圍內的所有變量或函數(其實就是執行上下文)。在 JavaScript
中,閉包被實現爲“內部函數”; 即,在另一個函數體內定義的函數。閉包的一個重要特性是內部函數仍然可以訪問外部函數的變量。
因此,在此示例中,由於x未在內部函數中定義,因此在外部函數的範圍內搜索已定義的變量x,該變量的值爲1。
如果理解不了這段內容,請自行 google 或者看一下我這篇關於閉包的博客:JavaScript 閉包機制的詳解
24.以下代碼將輸出上面內容到控制檯?這段代碼有什麼問題,如何解決?
var hero = {
_name: 'John Doe',
getSecretIdentity: function (){
return this._name;
}
};
var stoleSecretIdentity = hero.getSecretIdentity;
console.log(stoleSecretIdentity());
console.log(hero.getSecretIdentity());
這個問題本質上來說,是有關 JavaScript
中 this
的指向問題,先放上答案:
undefined
John Doe
爲什麼呢?我們先看第一個函數 stoleSecretIdentity()
,它的執行上下文是哪?很明顯是window
,而第二個函數 hero.getSecretIdentity()
呢?執行上下文是 hero
,這就是他們兩的輸出結果不同的原因所在。
至於有關 this 的指向問題的詳細解釋,你可以 google 一下,也可以看看我這篇文章::javascript this探究
25.實現一個函數,其功能是在給定頁面上的DOM元素的情況下,訪問元素本身及其所有後代(而不僅僅是其直接子元素)。對於訪問的每個元素,函數應該將該元素傳遞給提供的回調函數。
函數的參數應該是: 1.一個DOM元素。 2.回調函數(以DOM元素爲參數)
訪問樹的所有元素是經典的優先深度優先搜索算法,看如下實例:
function Traverse(p_element,p_callback) {
p_callback(p_element);
var list = p_element.children;
// 把len緩存起來在數據量大時可以提高部分性能
for (var i = 0,len = list.length;i < len; i++) {
Traverse(list[i],p_callback); // recursive call
}
}
26.以下代碼的輸出是什麼?
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments[0]();
}
};
obj.method(fn, 1);
很明顯這又是一道this
指向的問題。但是它比之前的題目更隱蔽,需要你完全理解this
在 JavaScript
中是怎麼調用的。
先看答案:
10
2
fn
被當成參數傳給了 method
函數,而 obj.method(fn, 1)
的執行上下文是 windows
,所以 fn
的調用者顯然就是 windows
,而此時 windows
環境中有一個 var length = 10;
,所以這時輸出的就是 10
;我們再來看 arguments[0]();
這句代碼,這句代碼等同於下面這句:
var arguments = [fn, 1];
var fn = arguments[0];
fn();
事實上 arguments
是什麼我們不再多說,如果你不瞭解我建議你多踏實基礎。所以調用方或則說此時這個 fn
的執行上下文是 arguments
,它的 length
是 2
,所以最後輸出 2
。
27.請考慮以下代碼,輸出內容是什麼,爲什麼?
(function () {
try {
throw new Error();
} catch (x) {
var x = 1, y = 2;
console.log(x);
}
console.log(x);
console.log(y);
})();
這道題其實考察的是 JavaScript
引擎在執行代碼時關於聲明提示的問題。以上代碼塊也可以認爲是下面這種形式:
(function () {
var x,y;
try {
throw new Error();
} catch (x) {
x = 1;
y = 2;
console.log('inner',x);
}
console.log('outer',x);
console.log('outer',y);
})();
我簡單解釋下,首先呢,JavaScript
中的變量提升我就不多說了,所以var x,y;
會在閉包的最頂端。其次就是這句代碼了:
catch (x) {
x = 1;
y = 2;
console.log(x);
}
這個 catch(x)
是關鍵,它意味着對這個塊級作用域內的x進行了局部聲明,而撇開了外部的全局聲明,所以 inner x
輸出的值應該是 1
,而 outer x
輸出的值應該是 undefined
。而 y
並沒有被局部重新聲明,所以 y
的輸出應該就是 2
。
28.下面這段代碼的輸出是什麼?
var x = 21;
var girl = function () {
console.log(x);
var x = 20;
};
girl ();
先說結果,輸出是 undefined
。也許有人會認爲是20,或者是21,但是很明顯你們都有理解上的誤差。我先舉個大家好理解的例子:
var x = 21;
var girl = function () {
console.log(x);
};
girl(); // 21
看到問題所在了嗎?關鍵就在於 var x = 20;
這句代碼,爲什麼?因爲它會發生變量提升,對 window
環境下的變量 a
進行重新聲明,就如同下面的代碼一樣:
var x = 21;
var girl = function () {
var x;
console.log(x);
x = 20;
};
girl ();
所以,打印的結果自然也就是 undefined
了。
29.在 JavaScript
中如何克隆一個對象?
首先我們要明白一個概念,那就是克隆分爲淺克隆和深克隆兩種類型,我們通常說的對象克隆,其實就是指的對象的深克隆。
關於淺克隆深克隆的更爲詳細的解釋,我就不在本文介紹了,大家可以看這篇博文 JavaScript 中的淺拷貝和深拷貝,下面我就簡單介紹下兩種深克隆的方式:
1.序列化與反序列化
const obj = {a:1, b:'str', c:{name:'waw', age:20}};
let objCopy = JSON.parse(JSON.stringify(obj));
objCopy.c.age = 18;
console.log(obj)
console.log(objCopy)
2.for...in
深遞歸
function isObject(obj){
return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}
function deepClone(obj){
let isArray = Array.isArray(obj);
var cloneObj = isArray ? [] : {};
for(let key in obj){
cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
}
return cloneObj;
}
let obj = {a:1, b:2, c:{name:'waw', age:10}};
let copyObj = deepClone(obj);
copyObj.c.age = 20;
console.log('obj', obj)
console.log('copyObj', copyObj)
30.以下這段代碼輸出什麼內容?
for (let i = 0; i < 5; i++) {
setTimeout(function() { console.log(i); }, i * 1000 );
}
這是一個典型的塊級作用域的問題。先說答案,將會打印出0,1,2,3,4
。
原因就在於 let
會形成一個塊級作用域,使得變量 i
只在 for
循環中生效。
31.以下這段代碼輸出什麼內容?
console.log(1 < 2 < 3);
console.log(3 > 2 > 1);
這道題我認爲實際上考察的是 JavaScript
的類型隱式轉換。
第一行代碼console.log(1 < 2 < 3);
,首先進行的是1 < 2
,表達式成立返回結果 true
,然後進行第二個表達式 true <3
,這時 true
被隱式轉換爲 1
, 即 1 < 3
,表達式成立,最終返回結果 true
。
第二行代碼console.log(3 > 2 > 1);
,首先進行的是3 > 2
,表達式成立返回結果 true
,然後進行第二個表達式 true > 1
,這時 true
被隱式轉換爲 1
, 即 1 > 1
,表達式不成立,最終返回結果 false
。
所以最後輸出結果應爲 true
和 false
。
32.JavaScript 中如何在數組的頭部和尾部插入元素?
對於傳統的ES5來說,我們可以通過以下方式來進行插入元素:
var arr = ['bb'];
arr.push('cc'); // ['bb', 'cc']
arr.unshift('aa') // ['aa', 'bb', 'cc']
對於ES6來說,我們可以使用擴展預算符進行解構賦值:
const arr = ['aa','bb'];
const arr2 = ['dd', ...arr, 'cc']; // ['dd', 'aa', 'bb', 'cc']
33.如果你有以下代碼:
var a = [1, 2, 3];
a) a[10] = 99;
b) console.log(a[6]);
a)這會導致崩潰嗎?
b)這個輸出是什麼?
先說答案:
a> 不會崩潰,JavaScript
會將中間空閒的元素位置爲空插槽(<7 empty items>
),實際輸出如下:
[ 1, 2, 3, <7 empty items>, 99 ]
這裏需要注意一點,這些空插槽在不同的環境下表現可能有所不同:
var a = [1, 2, 3];
a[10] = 99;
for(let i = 0; i< a.length;i++){
a[i] = 7;
}
console.log(a) // [7, 7, 7, 7, 7, 7, 7]
顯然數組 a
的元素會變成7。
我們再看一個例子:
var a = [1, 2, 3];
a[10] = 99;
a.map(e => 7);
console.log(a) // [ 1, 2, 3, <7 empty items>, 99 ]
此時的結果卻又不同於 for
循環,原因是 empty items
並沒有在 map
中被賦值,而是保留了下來。
b> 很明顯輸出會是 undefined
。
34.typeof undefined == typeof NULL
會輸出什麼結果?
這道題是典型的心機題,因爲此題中的 NULL
並不是我們所熟知的那個 null
,原因嘛,自然是 JavaScript
中是區分大小寫的。
如果是 typeof undefined == typeof null
,那麼輸出結果自然是 false
, 因爲typeof undefined = "undefined"
, 而 typeof null = "object"
。
而當 typeof undefined == typeof NULL
時,自然輸出結果是 true
了,因爲此時 NULL
相當於一個未定義的變量,typeof NULL = "undefined"
。
35.下面的代碼會返回什麼?
console.log(typeof typeof 1);
這道題主要考察你對 MDN
熟不熟,我們看下 MDN
上對 typeof
操作符的定義:
typeof 操作符返回一個字符串,表示未經計算的操作數的類型。
所以答案顯而易見就是 string
。
36.下面的代碼會返回什麼?
var b = 1;
function outer(){
var b = 2;
function inner(){
b++;
var b = 3;
console.log(b)
}
inner();
}
outer();
這是一道考察 JavaScript
作用域鏈的問題,先說答案:3
。
要確定爲什麼,只需要確定兩點:
- 輸出的代碼處在哪個作用域。
- 從此作用域往上去找需要輸出的變量,找到實例爲止。
在函數 inner
中是需要輸出的作用域,而 inner
中本就有變量 b
的實例,所以 b
爲 3。
此外,再說說在 inner
中的變量提升,inner
中的 b
將按照以下順序執行:
function inner(){
var b;
b++; // b is undefined
b++; // b is NaN
b = 3; // b is 3
console.log // 3
}