JAVAScript 是一種功能強大的語言,是網(wǎng)絡(luò)的主要構(gòu)建塊之一。這種強大的語言也有一些怪癖。例如,您是否知道 0 === -0 的計算結(jié)果為 true,或者 Number("") 的結(jié)果為 0?
問題是,有時這些怪癖會讓你摸不著頭腦,甚至質(zhì)疑 Brendon Eich 發(fā)明 JavaScript 的那一天。好吧,重點不在于 JavaScript 是一種糟糕的編程語言,或者像它的批評者所說的那樣它是邪惡的。所有編程語言都有某種與之相關(guān)的奇怪之處,JavaScript 也不例外。
因此,在今天這篇文章中,我們將會看到一些重要的 JavaScript 面試問題的深入解釋。我的目標是徹底解釋這些面試問題,以便我們能夠理解基本概念,并希望在面試中解決其他類似問題。
1、仔細觀察 + 和 - 運算符
console.log(1 + '1' - 1);
您能猜出 JavaScript 的 + 和 - 運算符在上述情況下的行為嗎?
當 JavaScript 遇到 1 + '1' 時,它會使用 + 運算符處理表達式。+ 運算符的一個有趣的屬性是,當操作數(shù)之一是字符串時,它更喜歡字符串連接。在我們的例子中,“1”是一個字符串,因此 JavaScript 隱式地將數(shù)值 1 強制轉(zhuǎn)換為字符串。因此,1 + '1' 變?yōu)?'1' + '1',結(jié)果是字符串 '11'。
現(xiàn)在,我們的等式是 '11' - 1。- 運算符的行為恰恰相反。無論操作數(shù)的類型如何,它都會優(yōu)先考慮數(shù)字減法。當操作數(shù)不是數(shù)字類型時,JavaScript 會執(zhí)行隱式強制轉(zhuǎn)換,將其轉(zhuǎn)換為數(shù)字。在本例中,“11”被轉(zhuǎn)換為數(shù)值 11,并且表達式簡化為 11 - 1。
把它們放在一起:
'11' - 1 = 11 - 1 = 10
2、復(fù)制數(shù)組元素
考慮以下 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);
在此代碼片段中,我們需要創(chuàng)建一個包含輸入數(shù)組的重復(fù)元素的新數(shù)組。初步檢查后,代碼似乎通過復(fù)制原始數(shù)組 arr 中的每個元素來創(chuàng)建一個新數(shù)組 newArr。然而,重復(fù)函數(shù)本身出現(xiàn)了一個關(guān)鍵問題。
重復(fù)函數(shù)使用循環(huán)來遍歷給定數(shù)組中的每個項目。但在循環(huán)內(nèi)部,它使用 push() 方法在數(shù)組末尾添加一個新元素。這使得數(shù)組每次都變得更長,從而產(chǎn)生循環(huán)永遠不會停止的問題。循環(huán)條件 (i < array.length) 始終保持為 true,因為數(shù)組不斷變大。這使得循環(huán)永遠持續(xù)下去,導(dǎo)致程序卡住。
為了解決數(shù)組長度不斷增長導(dǎo)致無限循環(huán)的問題,可以在進入循環(huán)之前將數(shù)組的初始長度存儲在變量中。
然后,您可以使用該初始長度作為循環(huán)迭代的限制。這樣,循環(huán)將僅針對數(shù)組中的原始元素運行,并且不會因添加重復(fù)項而受到數(shù)組增長的影響。這是代碼的修改版本:
function duplicate(array) {
var initialLength = array.length; // Store the initial length
for (var i = 0; i < initialLength; i++) {
array.push(array[i]); // Push a duplicate of each element
}
return array;
}
const arr = [1, 2, 3];
const newArr = duplicate(arr);
console.log(newArr);
輸出將顯示數(shù)組末尾的重復(fù)元素,并且循環(huán)不會導(dǎo)致無限循環(huán):
[1, 2, 3, 1, 2, 3]
3、原型和__proto__之間的區(qū)別
原型屬性是與 JavaScript 中的構(gòu)造函數(shù)相關(guān)的屬性。構(gòu)造函數(shù)用于在 JavaScript 中創(chuàng)建對象。定義構(gòu)造函數(shù)時,還可以將屬性和方法附加到其原型屬性。
然后,從該構(gòu)造函數(shù)創(chuàng)建的對象的所有實例都可以訪問這些屬性和方法。因此,prototype 屬性充當在實例之間共享的方法和屬性的公共存儲庫。
考慮以下代碼片段:
// Constructor function
function Person(name) {
this.name = name;
}
// Adding a method to the prototype
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}.`);
};
// Creating instances
const person1 = new Person("HAIder Wain");
const person2 = new Person("Omer Asif");
// Calling the shared method
person1.sayHello(); // Output: Hello, my name is Haider Wain.
person2.sayHello(); // Output: Hello, my name is Omer Asif.
在此示例中,我們有一個名為 Person 的構(gòu)造函數(shù)。通過使用 sayHello 之類的方法擴展 Person.prototype,我們將此方法添加到所有 Person 實例的原型鏈中。這允許 Person 的每個實例訪問和利用共享方法。而不是每個實例都有自己的方法副本。
另一方面, __proto__ 屬性(通常發(fā)音為“dunder proto”)存在于每個 JavaScript 對象中。在 JavaScript 中,除了原始類型之外,所有東西都可以被視為對象。這些對象中的每一個都有一個原型,用作對另一個對象的引用。__proto__ 屬性只是對此原型對象的引用。當原始對象不具備屬性和方法時,原型對象用作屬性和方法的后備源。默認情況下,當您創(chuàng)建對象時,其原型設(shè)置為 Object.prototype。
當您嘗試訪問對象的屬性或方法時,JavaScript 會遵循查找過程來查找它。這個過程涉及兩個主要步驟:
對象自己的屬性:JavaScript 首先檢查對象本身是否直接擁有所需的屬性或方法。如果在對象中找到該屬性,則直接訪問和使用它。
原型鏈查找:如果在對象本身中找不到該屬性,JavaScript 將查看該對象的原型(由 __proto__ 屬性引用)并在那里搜索該屬性。此過程在原型鏈上遞歸地繼續(xù),直到找到屬性或查找到達 Object.prototype。
如果即使在 Object.prototype 中也找不到該屬性,JavaScript 將返回 undefined,表明該屬性不存在。
4、范圍
編寫 JavaScript 代碼時,理解作用域的概念很重要。范圍是指代碼不同部分中變量的可訪問性或可見性。在繼續(xù)該示例之前,如果您不熟悉提升以及 JavaScript 代碼的執(zhí)行方式,可以從此鏈接了解它。這將幫助您更詳細地了解 JavaScript 代碼的工作原理。
讓我們仔細看看代碼片段:
function foo() {
console.log(a);
}
function bar() {
var a = 3;
foo();
}
var a = 5;
bar();
該代碼定義了 2 個函數(shù) foo() 和 bar() 以及一個值為 5 的變量 a。所有這些聲明都發(fā)生在全局范圍內(nèi)。在 bar() 函數(shù)內(nèi)部,聲明了一個變量 a 并賦值為 3。那么當調(diào)用 thebar() 函數(shù)時,你認為它會打印 a 的值是多少?
當 JavaScript 引擎執(zhí)行此代碼時,聲明全局變量 a 并為其賦值 5。然后,調(diào)用 bar() 函數(shù)。在 bar() 函數(shù)內(nèi)部,聲明了一個局部變量 a 并賦值為 3。該局部變量 a 與全局變量 a 不同。之后,從 bar() 函數(shù)內(nèi)部調(diào)用 foo() 函數(shù)。
在 foo() 函數(shù)內(nèi)部,console.log(a) 語句嘗試記錄 a 的值。由于 foo() 函數(shù)的作用域內(nèi)沒有定義局部變量 a,JavaScript 會查找作用域鏈以找到最近的名為 a 的變量。作用域鏈是指函數(shù)在嘗試查找和使用變量時可以訪問的所有不同作用域。
現(xiàn)在,我們來解決 JavaScript 將在哪里搜索變量 a 的問題。它會在 bar 函數(shù)的范圍內(nèi)查找,還是會探索全局范圍?事實證明,JavaScript 將在全局范圍內(nèi)進行搜索,而這種行為是由稱為詞法范圍的概念驅(qū)動的。
詞法作用域是指函數(shù)或變量在代碼中編寫時的作用域。當我們定義 foo 函數(shù)時,它被授予訪問其自己的本地作用域和全局作用域的權(quán)限。無論我們在哪里調(diào)用 foo 函數(shù),無論是在 bar 函數(shù)內(nèi)部還是將其導(dǎo)出到另一個模塊并在那里運行,這個特征都保持一致。詞法范圍不是由我們調(diào)用函數(shù)的位置決定的。
這樣做的結(jié)果是輸出始終相同:在全局范圍內(nèi)找到的 a 值,在本例中為 5。
但是,如果我們在 bar 函數(shù)中定義了 foo 函數(shù),則會出現(xiàn)不同的情況:
function bar() {
var a = 3;
function foo() {
console.log(a);
}
foo();
}
var a = 5;
bar();
在這種情況下, foo 的詞法作用域?qū)齻€不同的作用域:它自己的局部作用域、 bar 函數(shù)的作用域和全局作用域。詞法范圍由編譯時將代碼放置在源代碼中的位置決定。
當此代碼運行時,foo 位于 bar 函數(shù)內(nèi)。這種安排改變了范圍動態(tài)。現(xiàn)在,當 foo 嘗試訪問變量 a 時,它將首先在其自己的本地范圍內(nèi)進行搜索。由于它在那里找不到 a,因此它將搜索范圍擴大到 bar 函數(shù)的范圍。你瞧,a 存在,其值為 3。因此,控制臺語句將打印 3。
5、對象強制
const obj = {
valueOf: () => 42,
toString: () => 27
};
console.log(obj + '');
值得探索的一個有趣的方面是 JavaScript 如何處理對象到原始值(例如字符串、數(shù)字或布爾值)的轉(zhuǎn)換。這是一個有趣的問題,測試您是否知道強制轉(zhuǎn)換如何與對象一起使用。
在字符串連接或算術(shù)運算等場景中處理對象時,這種轉(zhuǎn)換至關(guān)重要。為了實現(xiàn)這一點,JavaScript 依賴于兩個特殊的方法:valueOf 和 toString。
valueOf 方法是 JavaScript 對象轉(zhuǎn)換機制的基本部分。當在需要原始值的上下文中使用對象時,JavaScript 首先在對象中查找 valueOf 方法。
如果 valueOf 方法不存在或未返回適當?shù)脑贾担琂avaScript 將回退到 toString 方法。該方法負責提供對象的字符串表示形式。
回到我們原來的代碼片段:
const obj = {
valueOf: () => 42,
toString: () => 27
};
console.log(obj + '');
當我們運行此代碼時,對象 obj 被轉(zhuǎn)換為原始值。在本例中,valueOf 方法返回 42,然后,由于與空字符串連接而隱式轉(zhuǎn)換為字符串。因此,代碼的輸出將為 42。
但是,如果 valueOf 方法不存在或未返回適當?shù)脑贾担琂avaScript 將回退到 toString 方法。讓我們修改一下之前的例子:
const obj = {
toString: () => 27
};
console.log(obj + '');
這里,我們刪除了 valueOf 方法,只留下 toString 方法,該方法返回數(shù)字 27。在這種情況下,JavaScript 將訴諸 toString 方法進行對象轉(zhuǎn)換。
6、理解對象鍵
在 JavaScript 中使用對象時,了解如何在其他對象的上下文中處理和分配鍵非常重要。考慮以下代碼片段并花一些時間猜測輸出:
let a = {};
let b = { key: 'test' };
let c = { key: 'test' };
a[b] = '123';
a[c] = '456';
console.log(a);
乍一看,這段代碼似乎應(yīng)該生成一個具有兩個不同鍵值對的對象 a。然而,由于 JavaScript 對對象鍵的處理方式,結(jié)果完全不同。
JavaScript 使用默認的 toString() 方法將對象鍵轉(zhuǎn)換為字符串。但為什么?在 JavaScript 中,對象鍵始終是字符串(或符號),或者它們通過隱式強制轉(zhuǎn)換自動轉(zhuǎn)換為字符串。當您使用字符串以外的任何值(例如數(shù)字、對象或符號)作為對象中的鍵時,JavaScript 會在將該值用作鍵之前在內(nèi)部將該值轉(zhuǎn)換為其字符串表示形式。
因此,當我們使用對象 b 和 c 作為對象 a 中的鍵時,兩者都會轉(zhuǎn)換為相同的字符串表示形式:[object Object]。由于這種行為,第二個賦值 a[b] = '123'; 將覆蓋第一個賦值 a[c] = '456';。
現(xiàn)在,讓我們逐步分解代碼:
- let a = {};:初始化一個空對象a。
- let b = { key: 'test' };: 創(chuàng)建一個對象 b,其屬性鍵值為 'test'。
- let c = { key: 'test' };: 定義另一個與 b 結(jié)構(gòu)相同的對象 c。
- a[b] = '123';:將對象a中鍵為[object Object]的屬性設(shè)置為值'123'。
a[c] = '456';:將對象 a 中鍵 [object Object] 相同屬性的值更新為 '456',替換之前的值。
兩個分配都使用相同的鍵字符串 [object Object]。結(jié)果,第二個賦值會覆蓋第一個賦值設(shè)置的值。
當我們記錄對象 a 時,我們觀察到以下輸出:
{ '[object Object]': '456' }
7、==運算符
console.log([] == ![]);
這個有點復(fù)雜。那么,您認為輸出會是什么?讓我們逐步評估一下。讓我們首先看看兩個操作數(shù)的類型:
typeof([]) // "object"
typeof(![]) // "boolean"
對于[]來說它是一個對象,這是可以理解的。JavaScript 中的一切都是對象,包括數(shù)組和函數(shù)。但是操作數(shù)![]如何具有布爾類型呢?讓我們試著理解這一點。當你使用 ! 對于原始值,會發(fā)生以下轉(zhuǎn)換:
假值:如果原始值是假值(例如 false、0、null、undefined、NaN 或空字符串 ''),則應(yīng)用 ! 會將其轉(zhuǎn)換為 true。
真值:如果原始值是真值(任何非假值),則應(yīng)用!會將其轉(zhuǎn)換為 false。
在我們的例子中,[] 是一個空數(shù)組,它是 JavaScript 中的真值。由于 [] 為真,所以 ![] 變?yōu)榧佟K裕覀兊谋磉_式就變成了:
[] == ![]
[] == false
現(xiàn)在讓我們繼續(xù)了解 == 運算符。每當使用 == 運算符比較 2 個值時,JavaScript 就會執(zhí)行抽象相等比較算法。
該算法有以下步驟:
正如您所看到的,該算法考慮了比較值的類型并執(zhí)行必要的轉(zhuǎn)換。
對于我們的例子,我們將 x 表示為 [],將 y 表示為 ![]。我們檢查了 x 和 y 的類型,發(fā)現(xiàn) x 是對象,y 是布爾值。由于 y 是布爾值,x 是對象,因此應(yīng)用抽象相等比較算法中的條件 7:
如果 Type(y) 為 Boolean,則返回 x == ToNumber(y) 的比較結(jié)果。
這意味著如果其中一種類型是布爾值,我們需要在比較之前將其轉(zhuǎn)換為數(shù)字。ToNumber(y) 的值是多少?正如我們所看到的,[] 是一個真值,否定則使其為假。結(jié)果,Number(false)為0。
[] == false
[] == Number(false)
[] == 0
現(xiàn)在我們有了比較 [] == 0,這次條件 8 開始發(fā)揮作用:
如果 Type(x) 是 String 或 Number 并且 Type(y) 是 Object,則返回比較結(jié)果 x == ToPrimitive(y)。
基于這個條件,如果其中一個操作數(shù)是對象,我們必須將其轉(zhuǎn)換為原始值。這就是 ToPrimitive 算法發(fā)揮作用的地方。我們需要將 [] x 轉(zhuǎn)換為原始值。數(shù)組是 JavaScript 中的對象。正如我們之前所看到的,當將對象轉(zhuǎn)換為基元時,valueOf 和 toString 方法就會發(fā)揮作用。
在這種情況下, valueOf 返回數(shù)組本身,它不是有效的原始值。因此,我們轉(zhuǎn)向 toString 進行輸出。將 toString 方法應(yīng)用于空數(shù)組會得到一個空字符串,這是一個有效的原語:
[] == 0
[].toString() == 0
"" == 0
將空數(shù)組轉(zhuǎn)換為字符串會得到一個空字符串“”,現(xiàn)在我們面臨比較:“”== 0。
現(xiàn)在,其中一個操作數(shù)是字符串類型,另一個操作數(shù)是數(shù)字類型,則條件 5 成立:
如果 Type(x) 是 String 并且 Type(y) 是 Number,則返回比較結(jié)果 ToNumber(x) == y。
因此,我們需要將空字符串“”轉(zhuǎn)換為數(shù)字,即為 0。
"" == 0
ToNumber("") == 0
0 == 0
最后,兩個操作數(shù)具有相同的類型并且條件 1 成立。由于兩者具有相同的值,因此,最終輸出為:
0 == 0 // true
到目前為止,我們在探索的最后幾個問題中使用了強制轉(zhuǎn)換,這是掌握 JavaScript 和在面試中解決此類問題的重要概念,這些問題往往會被問到很多。
8、閉包
這是與閉包相關(guān)的最著名的面試問題之一:
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log('Index: ' + i + ', element: ' + arr[i]);
}, 3000);
}
如果您知道輸出,那就好了。那么,讓我們嘗試理解這個片段。從表面上看,這段代碼片段將為我們提供以下輸出:
Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21
但這里的情況并非如此。由于閉包的概念以及 JavaScript 處理變量作用域的方式,實際的輸出會有所不同。當延遲 3000 毫秒后執(zhí)行 setTimeout 回調(diào)時,它們都將引用同一個變量 i,循環(huán)完成后該變量的最終值為 4。結(jié)果,代碼的輸出將是:
Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined
出現(xiàn)此行為的原因是 var 關(guān)鍵字沒有塊作用域,并且 setTimeout 回調(diào)捕獲對同一 i 變量的引用。當回調(diào)執(zhí)行時,它們都會看到 i 的最終值,即 4,并嘗試訪問未定義的 arr[4]。
為了實現(xiàn)所需的輸出,您可以使用 let 關(guān)鍵字為循環(huán)的每次迭代創(chuàng)建一個新范圍,確保每個回調(diào)捕獲 i 的正確值:
const arr = [10, 12, 15, 21];
for (let i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log('Index: ' + i + ', element: ' + arr[i]);
}, 3000);
}
通過此修改,您將獲得預(yù)期的輸出:
Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21
使用 let 在每次迭代中為 i 創(chuàng)建一個新的綁定,確保每個回調(diào)引用正確的值。
通常,開發(fā)人員已經(jīng)熟悉涉及 let 關(guān)鍵字的解決方案。然而,面試有時會更進一步,挑戰(zhàn)你在不使用 let 的情況下解決問題。在這種情況下,另一種方法是通過立即調(diào)用循環(huán)內(nèi)的函數(shù)(IIFE)來創(chuàng)建閉包。這樣,每個函數(shù)調(diào)用都有自己的 i 副本。您可以這樣做:
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
(function(index) {
setTimeout(function() {
console.log('Index: ' + index + ', element: ' + arr[index]);
}, 3000);
})(i);
}
在此代碼中,立即調(diào)用的函數(shù) (function(index) { ... })(i); 為每次迭代創(chuàng)建一個新范圍,捕獲 i 的當前值并將其作為索引參數(shù)傳遞。這確保每個回調(diào)函數(shù)都有自己單獨的索引值,防止與閉包相關(guān)的問題并為您提供預(yù)期的輸出:
Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21
最后總結(jié)
以上就是我今天這篇文章想與您分享的8個關(guān)于JS的前端面試題, 我希望這篇文章對您的面試準備之旅有所幫助。
如果您還有任何疑問,請在留言區(qū)給我們留言,我們一起交流學(xué)習進步。