被難倒了! 針對(duì)高級(jí)前端的八個(gè)級(jí)JavaScript面試問題
JavaScript 是一種功能強(qiáng)大的語言,也是構(gòu)建現(xiàn)代 Web 的基礎(chǔ)之一。這種強(qiáng)大的語言也有一些自己的怪癖。例如,你知道 0 === -0 會(huì)計(jì)算為 true,或者 Number("") 會(huì)返回 0 嗎?
有時(shí)候,這些怪癖會(huì)讓你百思不得其解,甚至讓你懷疑 Brendan Eich 在發(fā)明 JavaScript 的那一天是不是狀態(tài)不佳。但這里的重點(diǎn)并不是說 JavaScript 是一種糟糕的編程語言,或者如其批評(píng)者所說的那樣,是一種“邪惡”的語言。所有的編程語言都有某種程度的怪癖,JavaScript 也不例外。
在這篇博客文章中,我們將深入解釋一些重要的 JavaScript 面試問題。我的目標(biāo)是徹底解釋這些面試問題,以便我們能夠理解背后的基本概念,并希望在面試中解決其他類似的問題。
1、仔細(xì)觀察 + 和 - 運(yùn)算符
console.log(1 + '1' - 1);
你能猜到在上面這種情況下,JavaScript 的 + 和 - 運(yùn)算符會(huì)有什么行為嗎?
當(dāng) JavaScript 遇到 1 + '1' 時(shí),它會(huì)使用 + 運(yùn)算符來處理這個(gè)表達(dá)式。+ 運(yùn)算符有一個(gè)有趣的特性,那就是當(dāng)其中一個(gè)操作數(shù)是字符串時(shí),它更傾向于執(zhí)行字符串的連接。在我們的例子中,'1' 是一個(gè)字符串,因此 JavaScript 隱式地將數(shù)字 1 轉(zhuǎn)換為字符串。因此,1 + '1' 變成了 '1' + '1',結(jié)果是字符串 '11'。
現(xiàn)在,我們的等式是 '11' - 1。- 運(yùn)算符的行為正好相反。它更傾向于執(zhí)行數(shù)字減法,而不考慮操作數(shù)的類型。當(dāng)操作數(shù)不是數(shù)字類型時(shí),JavaScript 會(huì)執(zhí)行隱式轉(zhuǎn)換,將它們轉(zhuǎn)換為數(shù)字。在這種情況下,'11' 被轉(zhuǎn)換為數(shù)字值 11,表達(dá)式簡(jiǎn)化為 11 - 1。
綜合考慮:
'11' - 1 = 11 - 1 = 10
2、數(shù)組元素的復(fù)制
考慮以下的 JavaScript 代碼,并嘗試找出其中的問題:
function duplicate(array) {
for (var i = 0; i < array.length; i++) {
array.push(array[i]);
}
return array;
}
const arr = [1, 2, 3];
const newArr = duplicate(arr);
console.log(newArr);
在這段代碼片段中,我們需要?jiǎng)?chuàng)建一個(gè)新數(shù)組,該數(shù)組包含輸入數(shù)組的重復(fù)元素。初步檢查后,代碼似乎通過復(fù)制原始數(shù)組 arr 中的每個(gè)元素來創(chuàng)建一個(gè)新數(shù)組 newArr。然而,在 duplicate 函數(shù)內(nèi)部出現(xiàn)了一個(gè)嚴(yán)重的問題。
duplicate 函數(shù)使用循環(huán)來遍歷給定數(shù)組中的每個(gè)項(xiàng)目。但在循環(huán)內(nèi)部,它使用 push() 方法在數(shù)組末尾添加新元素。這導(dǎo)致數(shù)組每次都會(huì)變長(zhǎng),從而產(chǎn)生一個(gè)問題:循環(huán)永遠(yuǎn)不會(huì)停止。因?yàn)閿?shù)組長(zhǎng)度不斷增加,循環(huán)條件(i < array.length)始終為真。這使得循環(huán)無限進(jìn)行下去,導(dǎo)致程序陷入僵局。
為了解決由于數(shù)組長(zhǎng)度增長(zhǎng)而導(dǎo)致的無限循環(huán)問題,可以在進(jìn)入循環(huán)之前將數(shù)組的初始長(zhǎng)度存儲(chǔ)在一個(gè)變量中。然后,可以使用這個(gè)初始長(zhǎng)度作為循環(huán)迭代的限制。這樣,循環(huán)只會(huì)針對(duì)數(shù)組中的原始元素進(jìn)行,并不會(huì)受到由于添加重復(fù)項(xiàng)而導(dǎo)致數(shù)組增長(zhǎng)的影響。以下是修改后的代碼:
function duplicate(array) {
var initialLength = array.length; // 存儲(chǔ)初始長(zhǎng)度
for (var i = 0; i < initialLength; i++) {
array.push(array[i]); // 推入每個(gè)元素的副本
}
return array;
}
const arr = [1, 2, 3];
const newArr = duplicate(arr);
console.log(newArr);
輸出將顯示數(shù)組末尾的重復(fù)元素,并且循環(huán)不會(huì)導(dǎo)致無限循環(huán):
[1, 2, 3, 1, 2, 3]
3、prototype 和 proto 的區(qū)別
prototype 屬性是與 JavaScript 中的構(gòu)造函數(shù)相關(guān)聯(lián)的屬性。構(gòu)造函數(shù)用于在 JavaScript 中創(chuàng)建對(duì)象。當(dāng)您定義一個(gè)構(gòu)造函數(shù)時(shí),還可以將屬性和方法附加到其 prototype 屬性上。這些屬性和方法然后變得可以被該構(gòu)造函數(shù)創(chuàng)建的所有對(duì)象實(shí)例訪問。因此,prototype 屬性充當(dāng)共享方法和屬性的通用存儲(chǔ)庫(kù)。
考慮以下代碼片段:
// 構(gòu)造函數(shù)
function Person(name) {
this.name = name;
}
// 添加一個(gè)方法到 prototype
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}.`);
};
// 創(chuàng)建實(shí)例
const person1 = new Person("Haider Wain");
const person2 = new Person("Omer Asif");
// 調(diào)用共享的方法
person1.sayHello(); // 輸出:Hello, my name is Haider Wain.
person2.sayHello(); // 輸出:Hello, my name is Omer Asif.
另一方面,__proto__ 屬性,通常讀作 "dunder proto",存在于每一個(gè) JavaScript 對(duì)象中。在 JavaScript 中,除了原始類型外,一切都可以被視為對(duì)象。每個(gè)這樣的對(duì)象都有一個(gè)原型,該原型作為對(duì)另一個(gè)對(duì)象的引用。__proto__ 屬性簡(jiǎn)單地是對(duì)這個(gè)原型對(duì)象的引用。
當(dāng)你試圖訪問對(duì)象上的一個(gè)屬性或方法時(shí),JavaScript 會(huì)進(jìn)行查找過程來找到它。這個(gè)過程主要涉及兩個(gè)步驟:
對(duì)象的自有屬性:JavaScript 首先檢查對(duì)象自身是否直接擁有所需的屬性或方法。如果在對(duì)象內(nèi)找到了該屬性,則直接訪問和使用。原型鏈查找:如果在對(duì)象自身沒有找到該屬性,JavaScript 將查看對(duì)象的原型(由 __proto__ 屬性引用)并在那里搜索該屬性。這個(gè)過程會(huì)遞歸地沿著原型鏈進(jìn)行,直到找到該屬性或直到查找達(dá)到 Object.prototype。如果在 Object.prototype 中甚至沒有找到該屬性,JavaScript 將返回 undefined,表示該屬性不存在。
4、作用域
當(dāng)編寫 JavaScript 代碼時(shí),理解作用域的概念非常重要。作用域指的是變量在代碼的不同部分的可訪問性或可見性。下面我們通過一個(gè)代碼片段來更仔細(xì)地了解這個(gè)概念:
function foo() {
console.log(a);
}
function bar() {
var a = 3;
foo();
}
var a = 5;
bar();
代碼定義了兩個(gè)函數(shù) foo() 和 bar(),以及一個(gè)值為5的變量 a。所有這些聲明都發(fā)生在全局作用域中。在bar()函數(shù)內(nèi)部,聲明了一個(gè)變量a并賦值為 3。那么當(dāng)bar()函數(shù)被調(diào)用時(shí),你認(rèn)為會(huì)輸出哪個(gè)值的a?
當(dāng)JavaScript引擎執(zhí)行這段代碼時(shí),全局變量a被聲明并賦值為5。然后調(diào)用了bar()函數(shù)。在bar()函數(shù)內(nèi)部,聲明了一個(gè)局部變量a并賦值為3。這個(gè)局部變量a與全局變量a是不同的。之后,從bar()函數(shù)內(nèi)部調(diào)用了foo()函數(shù)。
在foo()函數(shù)內(nèi)部,console.log(a)語句試圖輸出變量a的值。由于在foo()函數(shù)的作用域內(nèi)沒有定義局部變量a,JavaScript會(huì)查找作用域鏈以找到最近的名為a的變量。
現(xiàn)在,我們來解答JavaScript將在哪里搜索變量a的問題。它會(huì)查找bar函數(shù)的作用域嗎,還是會(huì)探索全局作用域?事實(shí)證明,JavaScript會(huì)在全局作用域中搜索,這種行為是由一個(gè)叫做詞法作用域的概念驅(qū)動(dòng)的。
詞法作用域是指函數(shù)或變量在代碼中被編寫時(shí)的作用域。當(dāng)我們定義了foo函數(shù),它被賦予了訪問自己的局部作用域和全局作用域的權(quán)限。這一特性在我們無論在哪里調(diào)用foo函數(shù)時(shí)都是一致的,無論是在bar函數(shù)內(nèi)部還是在其他模塊中運(yùn)行。詞法作用域并不是由我們?cè)谀睦镎{(diào)用函數(shù)來決定的。
最終結(jié)果是,輸出始終是全局作用域中找到的a的值,在這個(gè)例子中是5。
然而,如果我們?cè)赽ar函數(shù)內(nèi)部定義了foo函數(shù),情況就會(huì)有所不同:
function bar() {
var a = 3;
function foo() {
console.log(a);
}
foo();
}
var a = 5;
bar();
在這種情況下,foo 的詞法作用域?qū)ㄈ齻€(gè)不同的作用域:它自己的局部作用域,bar 函數(shù)的作用域,以及全局作用域。詞法作用域是由你在源代碼中放置代碼的位置在編譯時(shí)決定的。
當(dāng)這段代碼運(yùn)行時(shí),foo 位于 bar 函數(shù)內(nèi)部。這種安排改變了作用域的動(dòng)態(tài)?,F(xiàn)在,當(dāng)foo試圖訪問變量a時(shí),它首先會(huì)在自己的局部作用域內(nèi)進(jìn)行搜索。由于沒有找到a,它會(huì)擴(kuò)大搜索范圍到bar函數(shù)的作用域。果然,那里存在一個(gè)值為3的a。因此,控制臺(tái)語句將輸出3。
5、對(duì)象強(qiáng)制類型轉(zhuǎn)換
const obj = {
valueOf: () => 42,
toString: () => 27
};
console.log(obj + '');
一個(gè)引人入勝的方面是探究JavaScript如何處理對(duì)象轉(zhuǎn)換為基本值,例如字符串、數(shù)字或布爾值。這是一個(gè)有趣的問題,測(cè)試你是否了解對(duì)象的強(qiáng)制類型轉(zhuǎn)換。
在像字符串連接或算術(shù)運(yùn)算這樣的場(chǎng)景中與對(duì)象一起工作時(shí),這種轉(zhuǎn)換至關(guān)重要。為了實(shí)現(xiàn)這一點(diǎn),JavaScript 依賴兩個(gè)特殊的方法:valueOf 和 toString。
valueOf 方法是JavaScript對(duì)象轉(zhuǎn)換機(jī)制的一個(gè)基礎(chǔ)部分。當(dāng)一個(gè)對(duì)象在需要基本值的上下文中被使用時(shí),JavaScript 首先會(huì)在對(duì)象內(nèi)部查找valueOf方法。在valueOf方法不存在或不返回適當(dāng)?shù)幕局档那闆r下,JavaScript會(huì)退回到toString方法。這個(gè)方法負(fù)責(zé)提供對(duì)象的字符串表示形式。
回到我們最初的代碼片段:
const obj = {
valueOf: () => 42,
toString: () => 27
};
console.log(obj + '');
當(dāng)我們運(yùn)行這段代碼時(shí),對(duì)象obj被轉(zhuǎn)換為一個(gè)基本值。在這種情況下,valueOf 方法返回42,然后由于與空字符串的連接,它被隱式地轉(zhuǎn)換為字符串。因此,代碼的輸出將是 42。
然而,在valueOf方法不存在或不返回適當(dāng)?shù)幕局档那闆r下,JavaScript會(huì)退回到toString方法。讓我們修改之前的示例:
const obj = {
toString: () => 27
};
console.log(obj + '');
在這里,我們已經(jīng)移除了 valueOf 方法,只留下了返回?cái)?shù)字27的toString方法。在這種情況下,JavaScript 將依賴 toString 方法進(jìn)行對(duì)象轉(zhuǎn)換。
6、理解對(duì)象鍵(Object Keys)
當(dāng)在JavaScript中使用對(duì)象時(shí),理解鍵是如何在其他對(duì)象的上下文中被處理和分配的非常重要。考慮以下代碼片段,并花點(diǎn)時(shí)間猜測(cè)輸出:
let a = {};
let b = { key: 'test' };
let c = { key: 'test' };
a[b] = '123';
a[c] = '456';
console.log(a);
乍一看,這段代碼似乎應(yīng)該生成一個(gè)具有兩個(gè)不同鍵值對(duì)的對(duì)象a。然而,由于JavaScript對(duì)對(duì)象鍵的處理方式,結(jié)果完全不同。
JavaScript 使用默認(rèn)的toString()方法將對(duì)象鍵轉(zhuǎn)換為字符串。為什么呢?在JavaScript中,對(duì)象鍵總是字符串(或 symbols),或者通過隱式強(qiáng)制轉(zhuǎn)換自動(dòng)轉(zhuǎn)換為字符串。當(dāng)你在對(duì)象中使用除字符串之外的任何值(例如,數(shù)字、對(duì)象或符號(hào))作為鍵時(shí),JavaScript將在使用它作為鍵之前內(nèi)部將該值轉(zhuǎn)換為其字符串表示形式。
因此,當(dāng)我們?cè)趯?duì)象a中使用對(duì)象b和c作為鍵時(shí),兩者都轉(zhuǎn)換為相同的字符串表示形式:[object Object]。由于這種行為,第二個(gè)賦值a[c] = '456';會(huì)覆蓋第一個(gè)賦值a[b] = '123';。
最終,當(dāng)我們記錄對(duì)象a時(shí),我們觀察到以下輸出:
{ '[object Object]': '456' }
7、雙等號(hào)運(yùn)算符
console.log([] == ![]);
這個(gè)有點(diǎn)復(fù)雜。那么,你認(rèn)為輸出會(huì)是什么呢?
這個(gè)問題相當(dāng)復(fù)雜。那么,你認(rèn)為輸出結(jié)果會(huì)是什么呢?讓我們一步一步地來評(píng)估。首先,讓我們看一下兩個(gè)操作數(shù)的類型:
typeof([]) // "object"
typeof(![]) // "boolean"
對(duì)于 [],它是一個(gè)對(duì)象,這是可以理解的,因?yàn)樵贘avaScript中,包括數(shù)組和函數(shù)在內(nèi)的一切都是對(duì)象。但操作數(shù) ![] 是如何具有布爾類型的呢?讓我們嘗試?yán)斫庖幌隆.?dāng)你使用 ! 與一個(gè)原始值(primitive value)一起時(shí),會(huì)發(fā)生以下轉(zhuǎn)換:
- Falsy Values(假值):如果原始值是一個(gè)假值(例如 false、0、null、undefined、NaN 或一個(gè)空字符串 ''),應(yīng)用 ! 將把它轉(zhuǎn)換為 true。
- Truthy Values(真值):如果原始值是一個(gè)真值(即任何不是假值的值),應(yīng)用 ! 將把它轉(zhuǎn)換為 false。
在我們的案例中,[] 是一個(gè)空數(shù)組,這在JavaScript中是一個(gè)真值。因?yàn)?nbsp;[] 是真值,![] 變成了 false。因此,我們的表達(dá)式變?yōu)椋?/p>
[] == ![]
[] == false
現(xiàn)在,讓我們繼續(xù)了解 == 運(yùn)算符。當(dāng)使用 == 運(yùn)算符比較兩個(gè)值時(shí),JavaScript會(huì)執(zhí)行“抽象相等性比較算法(Abstract Equality Comparison Algorithm)”。這個(gè)算法會(huì)考慮比較值的類型并進(jìn)行必要的轉(zhuǎn)換。
在我們的情況中,讓我們把 x 記作 [],y 記作 ![]。我們檢查了 x 和 y 的類型,并發(fā)現(xiàn) x 是對(duì)象,y 是布爾值。由于 y 是布爾值,x 是對(duì)象,算法的第7個(gè)條件被應(yīng)用:
如果 Type(y) 是 Boolean,則返回 x == ToNumber(y) 的比較結(jié)果。
這意味著如果其中一個(gè)類型是布爾值,我們需要在比較之前將其轉(zhuǎn)換為數(shù)字。ToNumber(y) 的值是多少呢?如我們所見,[] 是一個(gè)真值,取反使其變?yōu)?nbsp;false。因此,Number(false) 是 0。
[] == false
[] == Number(false)
[] == 0
現(xiàn)在我們有了 [] == 0 的比較,這次算法的第8個(gè)條件起作用:
如果 Type(x) 是 String 或 Number,而 Type(y) 是 Object,則返回 x == ToPrimitive(y) 的比較結(jié)果。
基于這個(gè)條件,如果其中一個(gè)操作數(shù)是對(duì)象,我們必須將其轉(zhuǎn)換為一個(gè)原始值。這就是“ToPrimitive算法”出現(xiàn)的地方。我們需要將 x(即 [])轉(zhuǎn)換為一個(gè)原始值。數(shù)組在JavaScript中是對(duì)象。當(dāng)將對(duì)象轉(zhuǎn)換為原始值時(shí),valueOf 和 toString 方法會(huì)起作用。在這種情況下,valueOf 返回?cái)?shù)組本身,這不是一個(gè)有效的原始值。因此,我們轉(zhuǎn)向 toString 以獲取輸出。將 toString 方法應(yīng)用于空數(shù)組會(huì)得到一個(gè)空字符串,這是一個(gè)有效的原始值:
[] == 0
[].toString() == 0
"" == 0
將空數(shù)組轉(zhuǎn)換為字符串給了我們一個(gè)空字符串 "",現(xiàn)在我們面對(duì)的比較是:"" == 0。
現(xiàn)在其中一個(gè)操作數(shù)的類型是字符串,另一個(gè)是數(shù)字,算法的第5個(gè)條件成立:
如果 Type(x) 是 String,而 Type(y) 是 Number,則返回 ToNumber(x) == y 的比較結(jié)果。
因此,我們需要將空字符串 "" 轉(zhuǎn)換為數(shù)字,這給了我們一個(gè) 0。
"" == 0
ToNumber("") == 0
0 == 0
最后,兩個(gè)操作數(shù)具有相同的類型和條件1成立。由于兩者具有相同的值,最終的輸出是:
0 == 0 // true
至此,我們已經(jīng)利用了強(qiáng)制轉(zhuǎn)換(coercion)來解決了我們探討的最后幾個(gè)問題,這是掌握J(rèn)avaScript和解決面試中這類常見問題的重要概念。我強(qiáng)烈建議你查看我的關(guān)于強(qiáng)制轉(zhuǎn)換的詳細(xì)博客文章。它以清晰和徹底的方式解釋了這個(gè)概念。這里是鏈接。