# JavaScript 面試常見問題
### 1. 請解釋 == 和 === 的差異
在JavaScript中,`==` 和 `===` 是用於比較兩個值是否相等的運算符,它們有不同的作用:
1. `==`(等於運算符):用於比較兩個值是否相等,但在比較之前會進行類型轉換(型別強制轉換。如果比較的兩個值的資料型別不同,JavaScript會嘗試將它們轉換為相同的資料型別,然後再進行比較。
2. `===`(嚴格等於運算符):用於比較兩個值是否相等,但它不會進行類型轉換。如果比較的兩個值的資料型別不同,即使值相同,也會被視為不相等。
下面是兩個運算符的示例:
```javascript=
var num = 5;
var str = "5";
console.log(num == str); // true,因為值相等,並且JavaScript將字符串轉換為數字進行比較
console.log(num === str); // false,因為值相等,但資料型別不同,不會進行類型轉換,因此被視為不相等
```
在上述示例中,`num` 和 `str` 的值都是5,但由於資料型別不同,`==` 運算符返回 `true`(進行了類型轉換),而 `===` 運算符返回 `false`(未進行類型轉換)。
使用哪個運算符取決於您的需求。如果您想要比較兩個值的值並允許進行類型轉換,則可以使用 `==`。如果您希望比較兩個值的值和資料型別,並且不允許類型轉換,則應使用 `===`。通常建議在可能的情況下使用 `===` 以避免意外的類型轉換和比較行為。
### 2. 所有的基本型別
JavaScript中有七種基本數據類型,也被稱為原始數據類型。以下是它們的列表以及每種類型的一些範例:
1. **String(字符串)**:用於表示文本數據。
```javascript=
let str = "Hello, World!";
```
2. **Number(數字)**:用於表示數值。
```javascript=
let num = 42;
```
3. **Boolean(布爾)**:用於表示真(true)或假(false)值。
```javascript=
let isTrue = true;
let isFalse = false;
```
4. **null**:用於表示空值或無值。
```javascript=
let emptyValue = null;
```
5. **undefined**:用於表示未定義的變數或缺少值。
```javascript=
let undefinedValue;
```
6. **BigInt(大整數)**:用於表示大整數(超出Number範圍的整數)。
```javascript=
let bigIntValue = 1234567890123456789012345678901234567890n;
```
7. **Symbol(符號)**:用於創建唯一且不可變的值。
```javascript=
const uniqueSymbol = Symbol("description");
```
這些是JavaScript的基本數據類型。您可以根據需要使用這些數據類型來創建變數和存儲數據。請注意,JavaScript還具有對象和陣列等複雜數據類型,它們可以用來組織和存儲更複雜的數據結構。
### 3. var、let、const 的差別?
`var`、`let` 和 `const` 是在JavaScript中用於聲明變數的不同關鍵字,它們之間有一些重要的差異,主要涉及到變數的作用域和可變性。
1. `var`:
- `var` 是ES5引入的變數聲明關鍵字。
- 具有函數作用域,如果在函數內聲明,則只在該函數內有效。
- 有提升(hoisting)現象,即變數聲明會被提升到它所在作用域的頂部。
- 沒有塊級作用域,例如,使用 `var` 在 `if` 語句中聲明的變數也在 `if` 語句外部可訪問。
- 可重複聲明相同的變數,不會報錯。
示例:
```javascript=
function exampleVar() {
if (true) {
var x = 10;
}
console.log(x); // 10,var 在 if 作用域外部也可訪問
}
exampleVar();
```
2. `let`:
- `let` 是ES6引入的變數聲明關鍵字。
- 具有塊級作用域,只在聲明它的塊(如 `{}`)內有效。
- 有提升(hoisting)現象,但變數不會被初始化,直到它的聲明被執行。
- 不允許重複聲明相同的變數,否則會報錯。
示例:
```javascript=
function exampleLet() {
if (true) {
let y = 20;
}
console.log(y); // ReferenceError: y is not defined,y 在 if 作用域外部不可訪問
}
exampleLet();
```
3. `const`:
- `const` 也是ES6引入的變數聲明關鍵字。
- 具有塊級作用域,只在聲明它的塊(如 `{}`)內有效。
- 有提升(hoisting)現象,但變數不會被初始化,直到它的聲明被執行。
- 必須立即初始化,且不能再次賦值。即它的值在聲明後不可更改,但對於對象和陣列,可以修改其內容。
示例:
```javascript=
function exampleConst() {
if (true) {
const z = 30;
}
console.log(z); // ReferenceError: z is not defined,z 在 if 作用域外部不可訪問
}
exampleConst();
```
總結:
- 使用 `let` 和 `const` 更常見於現代JavaScript,因為它們提供了更好的作用域控制和避免了一些錯誤。
- 使用 `const` 來聲明不需要重新賦值的變數,並且優先使用 `let` 來聲明需要重新賦值的變數。
- 建議不再使用 `var`,除非需要兼容舊版本的JavaScript。
### 4. 什麼是作用域(Scope)
在 JavaScript 中,作用域(Scope)是一個非常重要的概念,它決定了程式碼中變數和函數的可訪問性。作用域控制著變數和函數的可見性和生命週期。在 JavaScript 中,主要有兩種類型的作用域:全局作用域和局部作用域。
1. **全局作用域(Global Scope)**:
- 在全局作用域中聲明的變數可以在程式碼的任何地方被訪問。
- 全局作用域中的變數對整個程式碼來說都是全局可見的。
2. **局部作用域(Local Scope)**:
- 局部作用域通常指函數內部的作用域。
- 在函數內部聲明的變數只能在該函數內部被訪問,它對函數外部是不可見的。
#### 範例
```javascript=
var globalVar = "我在全局作用域";
function myFunction() {
var localVar = "我在局部作用域";
console.log(globalVar); // 輸出: 我在全局作用域
console.log(localVar); // 輸出: 我在局部作用域
}
myFunction();
console.log(globalVar); // 輸出: 我在全局作用域
console.log(localVar); // 錯誤: Uncaught ReferenceError: localVar is not defined
```
在這個範例中:
- `globalVar` 是一個全局變數,它可以在函數內外被訪問。
- `localVar` 是一個局部變數,它只能在 `myFunction` 函數內被訪問。當我們嘗試在函數外部訪問它時,會拋出一個錯誤。
了解作用域對於編寫可靠且易於維護的 JavaScript 程式碼至關重要。它幫助程式設計師避免命名衝突和意外的行為,並提高程式碼的模組化。
### 5. 什麼是 hoisting?
在 JavaScript 中,提升(Hoisting)是指函數和變數聲明在程式碼執行前被提升至其作用域的頂部的行為。這意味著不管聲明實際出現在哪裡,它們都會被提升至作用域的最上方。需要注意的是,提升只適用於聲明本身,而不涉及賦值或其他邏輯運算。
#### 變數提升(Variable Hoisting)
在 JavaScript 中,使用 `var` 關鍵字聲明的變數會被提升。這意味著變數可以在聲明之前被訪問,此時變數的值為 `undefined`。
#### 範例:
```javascript=
console.log(myVar); // 輸出: undefined
var myVar = 5;
console.log(myVar); // 輸出: 5
```
在這個例子中,雖然 `myVar` 在第一次 `console.log` 調用之後才被聲明和賦值,但由於變數提升,它在第一次 `console.log` 調用時已經存在於作用域中,只是它的值為 `undefined`。
#### 函數提升(Function Hoisting)
函數聲明(而非函數表達式)也會被提升到其包含作用域的頂部,這意味著函數可以在實際聲明之前被調用。
#### 範例:
```javascript=
console.log(myFunction()); // 輸出: "Hello, world!"
function myFunction() {
return "Hello, world!";
}
```
在這個例子中,`myFunction` 函數在被實際聲明之前就被調用了。由於函數提升,這是合法且可行的。
#### 注意點
- 使用 `let` 和 `const` 關鍵字聲明的變數不會發生提升,嘗試在聲明前訪問這些變數會導致 `ReferenceError`。
- 函數表達式(包括箭頭函數)不會被提升。
提升是 JavaScript 的一個重要特性,了解它有助於避免潛在的錯誤和混淆。
### 6. 什麼是閉包(Closure)?用途是什麼,並給出一個例子?
閉包(Closure)是 JavaScript 中的一個核心概念,它允許函數訪問並操作該函數外部的變數。簡單來說,當一個函數記住並訪問它的外部作用域中的變數時,即使該函數在其外部作用域之外執行,這種現象稱為閉包。
#### 用途
閉包的主要用途包括:
1. **維持私有變數**:使用閉包可以創建私有變數,這些變數不能直接從外部訪問,僅通過特定的函數訪問和修改。
2. **模擬私有方法**:在 JavaScript 中,閉包可以用來模擬私有方法,這是封裝的一種形式。
3. **創建具有特定環境的函數**:閉包使得函數可以攜帶自己的環境和作用域,有利於編寫模組化代碼。
4. **實現柯里化(Currying)**:閉包允許部分應用函數,這是函數式編程中的一個重要技巧。
#### 維持私有變數
以下是一個閉包的範例,展示了如何使用閉包創建私有變數和方法:
```javascript=
function createCounter() {
let count = 0; // 私有變數
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 輸出: 1
console.log(counter.increment()); // 輸出: 2
console.log(counter.getCount()); // 輸出: 2
console.log(counter.decrement()); // 輸出: 1
```
在這個例子中,`createCounter` 函數返回了一個包含三個方法的對象。這三個方法都是閉包,它們共享相同的外部作用域(即 `createCounter` 函數的作用域),並且可以訪問和操作私有變數 `count`。由於 `count` 變數位於 `createCounter` 函數內部,它無法被外部直接訪問,這保證了其私有性和安全性。
通過這種方式,閉包不僅能夠封裝和保護數據,還能為函數創建特定的環境和上下文。
當然可以。下面分別提供了模擬私有方法、創建具有特定環境的函數,以及實現柯里化的 JavaScript 範例:
#### 模擬私有方法
```javascript=
function createBankAccount(initialBalance) {
let balance = initialBalance; // 私有變數
function deposit(amount) {
balance += amount;
}
function withdraw(amount) {
if (balance >= amount) {
balance -= amount;
} else {
console.log('餘額不足');
}
}
function getBalance() {
return balance;
}
return {
deposit,
withdraw,
getBalance
};
}
const account = createBankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 輸出: 150
account.withdraw(20);
console.log(account.getBalance()); // 輸出: 130
```
在這個範例中,`deposit`、`withdraw` 和 `getBalance` 函數是私有方法,它們可以訪問和操作 `balance` 這個私有變數。
#### 創建具有特定環境的函數
```javascript=
function createGreeting(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}
const greetHello = createGreeting('Hello');
const greetHi = createGreeting('Hi');
console.log(greetHello('Alice')); // 輸出: Hello, Alice!
console.log(greetHi('Bob')); // 輸出: Hi, Bob!
```
在這個範例中,`createGreeting` 函數創建並返回一個新的函數,該函數閉包了 `greeting` 參數。每個返回的函數都有自己的環境和作用域。
#### 實現柯里化(Currying)
```javascript=
function multiply(a, b) {
return a * b;
}
function curriedMultiply(a) {
return function(b) {
return a * b;
}
}
let a_curry = curriedMultiply(2)
let b_curry = a_curry(3)
console.log(b_curry);// 輸出: 6
console.log(curriedMultiply(2)(3)); // 輸出: 6
```
`curriedMultiply` 是一個柯里化版本的 `multiply` 函數。柯里化的關鍵在於,它接受一個參數 `a`,並返回一個函數,這個函數接受另一個參數 `b`,然後返回 `a * b` 的結果。
我們使用 `curriedMultiply(2)` 創建了一個新的函數 `a_curry`,這個函數在內部固定了參數 `a` 的值為 `2`。換句話說,`a_curry` 是一個單參數函數,當我們調用它並傳入 `b` 時,它將返回 `2 * b` 的結果。
我們使用 `a_curry(3)`,這樣就完成了柯里化的過程。它實際上是將 `b` 參數傳遞給了 `a_curry`,所以最終的計算是 `2 * 3`,結果為 `6`。
直接調用 `curriedMultiply(2)(3)`,這樣也會直接印出 `6`。
### 7. 什麼是 callback function?
在 JavaScript 中,回調函數(Callback Function)是一種可以作為參數傳遞給另一個函數,並在該函數內部某一時刻或某一事件發生後被調用的函數。回調函數廣泛用於異步程式設計,如處理 I/O 操作、事件處理或時間延遲等場景。
#### 1. 異步操作的回調函數
```javascript=
function fetchData(callback) {
setTimeout(() => {
callback('數據加載完畢');
}, 1000);
}
fetchData(function(data) {
console.log(data); // 1 秒後輸出: 數據加載完畢
});
console.log('開始加載數據...'); // 立即輸出
//---打印結果---
//開始加載數據... // 立即輸出
//數據加載完畢 // 1 秒後輸出
//---打印結果---
```
#### 2. 事件處理的回調函數
```javascript=
document.getElementById('myButton').addEventListener('click', function() {
console.log('按鈕被點擊了');
});
```
這個範例中,回調函數被用作事件處理器。當按鈕(`myButton`)被點擊時,回調函數被調用,並輸出一條信息到控制台。
#### 3. 高階函數中的回調函數
```javascript=
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(function(number) {
return number * 2;
});
console.log(doubled); // 輸出: [2, 4, 6, 8, 10]
```
在這個範例中,`map` 函數是一個高階函數,它接受一個回調函數作為參數。這個回調函數對數組 `numbers` 中的每個元素進行操作,並返回一個新的數組。
回調函數是 JavaScript 中一個重要的概念,它使得非同步操作、事件處理和數組方法等更加靈活和強大。然而,過度使用回調函數可能導致所謂的「回調地獄」,這是當多個回調函數嵌套和連鎖時出現的一種複雜和難以維護的代碼結構。因此,在現代 JavaScript 開發中,經常使用 Promise 和 async/await 這樣的特性來更優雅地處理異步操作。
### 8. 什麼是callback 地獄?
Callback 地獄(Callback Hell)是指當您在程式碼中使用大量的回調函數(callbacks)並且這些回調函數嵌套在彼此內部時,導致程式碼變得難以理解和維護的情況。這種情況通常出現在處理異步操作的情境中,如文件讀取、網絡請求或定時器。
Callback 地獄的主要問題在於程式碼的縮排(indentation)層次變得非常深,造成程式碼難以閱讀和理解。此外,錯誤處理變得更加困難,因為您需要追蹤多個回調函數中的錯誤。
以下是一個簡單的 JavaScript 範例,演示了 Callback 地獄的情況,其中有三個異步操作的回調函數嵌套:
```javascript=
getDataFromServer(function(data1) {
processData1(data1, function(data2) {
processData2(data2, function(data3) {
// 在這裡處理 data3
}, function(error) {
// 處理 data2 錯誤
});
}, function(error) {
// 處理 data1 錯誤
});
}, function(error) {
// 處理從伺服器獲取數據的錯誤
});
```
這種程式碼結構雖然可以工作,但讓程式碼變得難以閱讀和維護,特別是當回調函數的數量增加時。為了解決 Callback 地獄,可以使用一些技術,如 Promise、async/await 或函數庫(例如,Async.js)來使異步程式碼更加可讀且易於管理。
### 9. 什麼是 event loop?
事件循環(Event Loop)是 JavaScript 運行環境的一個重要概念,它負責處理異步操作和事件。事件循環確保 JavaScript 在處理非同步任務時能夠保持效率,並確保代碼不會被阻塞。
以下是一個簡單的 JavaScript 範例,演示了事件循環的基本工作方式:
```javascript=
console.log("開始");
setTimeout(function() {
setTimeout(function(){
console.log("非同步操作 1 完成")
},1000)
console.log("非同步操作 2 完成");
}, 1000);
setTimeout(function() {
console.log("非同步操作 3 完成");
}, 500);
console.log("結束");
//---打印結果---
//開始
//結束
//非同步操作 3 完成 // 0.5 秒後
//非同步操作 2 完成 // 1 秒後
//非同步操作 1 完成 // 2 秒後
//---打印結果---
```
事件循環確保非同步操作在背後運行,而不會阻塞代碼的執行,使得 JavaScript 可以處理多個非同步任務並保持反應靈敏。
### 10. AJAX vs AJAS
AJAX(Asynchronous JavaScript and XML)和 AJAS(Asynchronous JavaScript and JSON)是兩種網頁開發技術,它們都用於在不重新加載整個頁面的情況下,從服務器獲取數據並動態更新網頁的內容。它們的主要區別在於處理數據的格式:
1. **AJAX:**
- AJAX 是一種利用 JavaScript 進行異步網路請求的技術,最初主要用於與服務器交換 XML 格式的數據。
- 雖然名稱中包含 XML,但 AJAX 也可以用於處理其他類型的數據,包括 JSON、HTML、純文本等。
2. **AJAS:**
- AJAS 指的是使用 AJAX 技術進行異步請求,但主要用於處理 JSON 格式的數據。
- JSON(JavaScript Object Notation)是一種更輕量、更易於閱讀和寫入的數據交換格式,已經成為現代 Web 開發中最受歡迎的數據格式之一。
當然可以。使用 `fetch` API,我們可以更簡潔地進行異步網路請求。以下是使用 `fetch` API 處理 JSON 和 XML 數據的範例:
#### 處理 JSON 數據(AJAS)
```javascript=
fetch('example.json')
.then(response => response.json()) // 解析 JSON 數據
.then(data => {
console.log(data); // 處理 JSON 數據
})
.catch(error => {
console.error('錯誤:', error);
});
```
在這個範例中,`fetch` 函數向 `'example.json'` 發送請求。當收到響應後,首先將其解析為 JSON,然後處理這些數據。
#### 處理 XML 數據(AJAX)
```javascript=
fetch('example.xml')
.then(response => response.text()) // 獲取文本響應
.then(str => (new window.DOMParser()).parseFromString(str, "text/xml")) // 將文本轉換為 XML
.then(data => {
console.log(data); // 處理 XML 數據
})
.catch(error => {
console.error('錯誤:', error);
});
```
這個範例中,我們使用 `fetch` 從 `'example.xml'` 獲取數據。首先將響應轉換為文本,然後使用 `DOMParser` 將文本轉換為 XML 格式,最後處理 XML 數據。
### 11. function declaration vs function expression 差別?
在 JavaScript 中,函數宣告(Function Declaration)和函數表達式(Function Expression)是定義函數的兩種不同方式,它們有一些關鍵的差異:
#### 1. 函數宣告(Function Declaration)
- **特點**:函數宣告會在程式碼執行之前被 JavaScript 引擎提升(Hoisting),這意味著你可以在函數宣告之前調用該函數。
- **語法**:
```javascript=
function functionName(parameters) {
// 函數體
}
```
**範例**:
```javascript=
console.log(square(5)); // 輸出:25
function square(number) {
return number * number;
}
```
在這個範例中,即使 `square` 函數的調用在其宣告之前,它仍能正常工作,因為函數宣告會被提升。
#### 2. 函數表達式(Function Expression)
- **特點**:函數表達式不會被提升。這意味著你必須先定義函數表達式,然後才能調用它。
- **語法**:
```javascript=
const functionName = function(parameters) {
// 函數體
};
```
- **範例**:
```javascript=
const square = function(number) {
return number * number;
};
console.log(square(5)); // 輸出:25
```
在這個範例中,我們使用函數表達式創建了 `square` 函數。如果嘗試在函數表達式之前調用 `square` 函數,將會導致錯誤。
#### 差異總結
- **提升(Hoisting)**:函數宣告會被提升,而函數表達式不會。
- **語法**:函數宣告具有特定的語法形式,而函數表達式則可以是匿名的。
- **使用時機**:函數表達式適合在需要將函數賦值給變數或傳遞給其他函數時使用;函數宣告則適合在全局或局部作用域直接定義函數時使用。
### 12. By value vs by reference
在 JavaScript 中,「按值傳遞(By Value)」和「按引用傳遞(By Reference)」是兩種不同的數據傳遞方式。理解這兩種概念對於明白如何在函數之間傳遞數據和修改數據非常重要。
#### 按值傳遞(By Value)
- 當數據是基本類型(如數字、字符串、布爾值)時,JavaScript 在將其傳遞給函數或賦值給另一個變數時,是按值傳遞的。這意味著創建了原始數據的一個副本。
#### 按引用傳遞(By Reference)
- 當數據是對象(包括數組和函數)時,JavaScript 會按引用傳遞。在這種情況下,傳遞的不是實際的對象,而是對該對象的引用(或指向該對象的指針)。因此,對該引用的任何修改都會影響原始對象。
#### 按值傳遞
```javascript=
let a = 10;
let b = a;
b = 20;
console.log(a); // 輸出: 10
console.log(b); // 輸出: 20
```
#### 按引用傳遞
```javascript=
let obj1 = { name: 'Alice' };
let obj2 = obj1;
obj2.name = 'Bob';
console.log(obj1); // 輸出: { name: "Bob" }
console.log(obj2); // 輸出: { name: "Bob" }
```
#### 比較表
| 特性 | 按值傳遞 | 按引用傳遞 |
|--------------|-------------------|-------------------|
| 類型 | 基本類型(數字、字符串、布爾值) | 對象(包括數組、函數) |
| 傳遞方式 | 傳遞副本 | 傳遞引用 |
| 對原始數據的影響 | 不會影響原始數據 | 會影響原始數據 |
#### 常見面試問題及解答
**問題**:JavaScript 中的基本類型和對象是如何傳遞的?
**解答**:在 JavaScript 中,基本類型(如數字、字符串、布爾值)是按值傳遞的,這意味著當它們被賦值給新的變數或作為參數傳遞給函數時,實際上傳遞的是它們的副本。對這些副本的任何修改都不會影響原始數據。相反,對象(包括數組和函數)是按引用傳遞的,這意味著傳遞的是對原始對象的引用,因此對這些引用的修改會直接影響原始對象。
### 13. 在不同情況下 this 指向對象會是什麼?
在 JavaScript 中,`this` 關鍵字的指向取決於函數是如何被調用的。不同的調用方式導致 `this` 的值有所不同。以下是幾種常見情況下 `this` 的指向:
#### 1. 全局上下文
在全局執行上下文中(在任何函數之外),`this` 指向全局對象。在瀏覽器中,全局對象是 `window`,而在 Node.js 中是 `global`。
**範例**:
```javascript=
console.log(this === window); // 在瀏覽器中輸出:true
```
#### 2. 函數調用
在普通函數調用中,`this` 通常指向全局對象(在嚴格模式下,`this` 會是 `undefined`)。
**範例**:
```javascript=
function showThis() {
console.log(this);
}
showThis(); // 在非嚴格模式下輸出:Window(瀏覽器環境)
```
#### 3. 方法調用
當函數作為對象的方法被調用時,`this` 指向該方法所屬的對象。
**範例**:
```javascript=
const obj = {
method: function() {
console.log(this);
}
};
obj.method(); // 輸出:obj
```
#### 4. 構造函數調用
使用 `new` 操作符調用函數時,`this` 指向新創建的對象。
**範例**:
```javascript=
function MyConstructor() {
this.value = 10;
}
const instance = new MyConstructor();
console.log(instance.value); // 輸出:10
```
#### 5. 明確設定 `this`
通過 `call`、`apply` 或 `bind` 方法,可以明確設定函數調用時 `this` 的值。
**範例**:
```javascript=
function showThis() {
console.log(this);
}
const obj = { a: 100 };
showThis.call(obj); // 輸出:obj
```
#### 比較表
| 情況 | `this` 指向 | 備註 |
|--------------|------------------|--------------------------|
| 全局上下文 | 全局對象(`window` / `global`) | |
| 函數調用 | 全局對象或 `undefined` | 取決於嚴格模式與否 |
| 方法調用 | 方法所屬的對象 | |
| 構造函數調用 | 新創建的對象 | 使用 `new` 操作符 |
| 明確設定 `this` | 指定的對象 | 通過 `call`、`apply`、`bind` |
#### 面試問題及解答
**問題**:在箭頭函數中,`this` 的值是什麼?
**解答**:箭頭函數不綁定自己的 `this`,而是捕獲其所在上下文的 `this` 值。這意味著箭頭函數內的 `this` 是在函數定義時而非執行時確定的。如果箭頭函數被包含在一個普通函數中,它的 `this` 值將是該普通函數的 `this` 值。
```javascript=
function regularFunction() {
const arrowFunction = () => {
console.log(this);
};
arrowFunction();
}
const obj = new regularFunction(); // 輸出:regularFunction 的新實例
```
在這個例子中,箭頭函數捕獲了 `regularFunction` 的 `this`,即其新創建的實例。
### 14. 什麼是原型鍊(prototype chain)?
原型鏈(Prototype Chain)是 JavaScript 中一個基本且重要的概念。它是基於原型繼承和對象間的委托機制構建的,允許對象繼承另一個對象的屬性和方法。
#### 原型鏈解釋
當你嘗試訪問一個 JavaScript 對象的屬性或方法時,解釋器首先檢查對象本身是否擁有該屬性或方法。如果沒有找到,它會接著查找對象的原型(`__proto__` 屬性指向的對象),以此類推,直到找到該屬性或方法,或者達到原型鏈的末端(通常是 `Object.prototype`)。如果在原型鏈的任何一級都沒有找到該屬性或方法,則返回 `undefined`。
#### 程式範例
```javascript=
function Animal(name) {
this.name = name;
}
Animal.prototype.getName = function() {
return this.name;
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
return "Woof!";
};
const myDog = new Dog("Max");
console.log(myDog.getName()); // 從 Animal 繼承的方法
console.log(myDog.bark()); // Dog 自己的方法
```
在這個範例中,`Dog` 繼承自 `Animal`。當我們呼叫 `myDog.getName()` 時,JavaScript 首先在 `myDog` 實例中尋找 `getName` 方法,未找到後沿原型鏈向上查找,在 `Animal.prototype` 中找到該方法。
#### 比較表
| 特性 | 原型鏈(Prototype Chain) |
|-----------------|------------------------------------------|
| 定義 | 一系列指向其他對象的連結,用於查找屬性和方法 |
| 功能 | 允許一個 JavaScript 對象繼承另一個對象的屬性和方法 |
| 繼承方式 | 通過原型對象間的連結實現繼承 |
| 查找過程 | 首先檢查對象自身,然後順著原型鏈向上查找直到找到為止 |
| 終點 | 通常終於 `Object.prototype`,其 `__proto__` 為 `null` |
#### 面試問題及解答
**問題**:請解釋 JavaScript 中的原型繼承是如何工作的。
**解答**:在 JavaScript 中,原型繼承是通過原型對象(prototype)實現的。每個函數都有一個 `prototype` 屬性,它指向一個對象,這個對象包含了可以由該函數的所有實例繼承的方法和屬性。當創建一個函數的新實例時,這個實例會繼承函數的原型上的屬性和方法。如果在實例上找不到某個屬性或方法,JavaScript 會沿著原型鏈向上查找,直到找到為止。這種機制允許對象共享方法,並節省記憶體。
### 15. 原型鍊 vs class
在 JavaScript 中,原型鏈(Prototype Chain)和 `class` 關鍵字都是用於實現繼承和對象導向編程的機制,但它們在概念和語法上有所不同。
#### 原型鏈(Prototype Chain)
1. **概念**:
- 原型鏈是基於原型的繼承機制,它允許對象繼承另一個對象的屬性和方法。
- 每個 JavaScript 對象都有一個原型,該原型也是一個對象。當查找一個對象的屬性時,如果在該對象上找不到,解釋器會在其原型上查找,依此類推,形成一個「鏈」。
2. **實現方式**:
- 透過函數的 `prototype` 屬性和構造函數來實現。
#### `class` 關鍵字
1. **概念**:
- `class` 是 ES6 引入的語法糖,它提供了一種更清晰、更符合傳統物件導向語言的方式來實現繼承。
- 雖然 `class` 使得代碼更接近傳統的物件導向語言,但在底層,JavaScript 仍然是使用基於原型的繼承機制。
2. **實現方式**:
- 使用 `class` 和 `extends` 關鍵字,提供了一種更熟悉的語法來創建對象和實現繼承。
#### 比較
| 特性 | 原型鏈 | `class` |
|--------------|------------------------------|----------------------------------|
| 概念 | 基於對象的原型屬性來實現繼承 | 提供了更傳統的類式繼承語法 |
| 語法 | 通過構造函數和修改原型對象實現繼承 | 使用 `class` 和 `extends` 實現繼承 |
| 繼承實現 | 透過原型對象間的鏈接 | 內部仍然透過原型鏈實現 |
| 易用性 | 直接操作原型可能較為複雜 | 提供了更易於理解和使用的繼承方式 |
#### 面試問題及解答
**問題**:JavaScript 的 `class` 關鍵字是否引入了新的繼承機制?
**解答**:不,`class` 關鍵字在 JavaScript 中並沒有引入新的繼承機制。它是一種語法糖,提供了一種更清晰、更符合傳統物件導向編程語言的方式來創建對象和實現繼承。在底層,JavaScript 仍然使用基於原型的繼承機制。這意味著使用 `class` 關鍵字創建的「類」實際上是函數,而繼承仍然是通過原型鏈來實現的。這使得代碼看起來更像是使用傳統的類基繼承,但實際上並未改變 JavaScript 的繼承本質。
### 16.callback vs Promise vs async
在 JavaScript 中,Callback、Promise 和 Async/Await 是處理異步操作的三種不同方法。每種方法都有其特點和使用場景。
#### Callback
1. **概念**:Callback 是一種將函數作為參數傳遞給另一個函數,並在某個事件或異步操作完成後被調用的技術。
2. **使用場景**:在 JavaScript 早期廣泛用於異步處理,例如事件處理和 I/O 操作。
3. **問題**:過度使用可能導致「回調地獄」,使代碼難以閱讀和維護。
**範例**:
```javascript=
function fetchData(callback) {
setTimeout(() => {
callback('數據加載完畢');
}, 1000);
}
fetchData(data => {
console.log(data); // 1 秒後輸出 "數據加載完畢"
});
```
#### Promise
1. **概念**:Promise 是一個表示異步操作最終完成或失敗的對象。
2. **使用場景**:提供了比回調更好的異步處理機制,解決了回調地獄的問題。
3. **特點**:支持鏈式調用(`then` 和 `catch`),使代碼更清晰。
**範例**:
```javascript=
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('數據加載完畢');
}, 1000);
});
}
fetchData().then(data => {
console.log(data); // 1 秒後輸出 "數據加載完畢"
});
```
#### Async/Await
1. **概念**:Async/Await 是基於 Promise 的語法,使異步代碼看起來像同步代碼。
2. **使用場景**:提供了一種更直觀的方式來處理異步操作。
3. **特點**:讓代碼更易讀、更易維護。
**範例**:
```javascript=
async function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('數據加載完畢');
}, 1000);
});
}
async function showData() {
const data = await fetchData();
console.log(data); // 1 秒後輸出 "數據加載完畢"
}
showData();
```
#### 比較
以下是一個回調地獄(Callback Hell)的範例,並展示如何使用 Promise 和 Async/Await 來優化這段代碼。
- **回調地獄範例**
假設我們有多個異步操作,每個操作完成後都需要執行下一個操作。在使用回調的情況下,代碼可能會變得深度嵌套並難以閱讀:
```javascript=
function operation1(callback) {
setTimeout(() => {
console.log("操作 1 完成");
callback();
}, 1000);
}
function operation2(callback) {
setTimeout(() => {
console.log("操作 2 完成");
callback();
}, 1000);
}
function operation3(callback) {
setTimeout(() => {
console.log("操作 3 完成");
callback();
}, 1000);
}
operation1(() => {
operation2(() => {
operation3(() => {
console.log("所有操作完成");
});
});
});
```
- **使用 Promise 優化**
使用 Promise 可以避免深度嵌套的回調,並提供更清晰的代碼結構:
```javascript=
function operation1() {
return new Promise(resolve => {
setTimeout(() => {
console.log("操作 1 完成");
resolve();
}, 1000);
});
}
function operation2() {
return new Promise(resolve => {
setTimeout(() => {
console.log("操作 2 完成");
resolve();
}, 1000);
});
}
function operation3() {
return new Promise(resolve => {
setTimeout(() => {
console.log("操作 3 完成");
resolve();
}, 1000);
});
}
operation1()
.then(operation2)
.then(operation3)
.then(() => {
console.log("所有操作完成");
});
```
- **使用 Async/Await 優化**
Async/Await 提供了一種更直觀的方式來處理異步操作,使代碼看起來像是同步的:
```javascript=
async function operation1() {
return new Promise(resolve => {
setTimeout(() => {
console.log("操作 1 完成");
resolve();
}, 1000);
});
}
async function operation2() {
return new Promise(resolve => {
setTimeout(() => {
console.log("操作 2 完成");
resolve();
}, 1000);
});
}
async function operation3() {
return new Promise(resolve => {
setTimeout(() => {
console.log("操作 3 完成");
resolve();
}, 1000);
});
}
async function performOperations() {
await operation1();
await operation2();
await operation3();
console.log("所有操作完成");
}
performOperations();
```
在這裡,我們使用了 `async` 函數和 `await` 關鍵字來等待每個操作的完成。這種方法讓代碼更加直觀、更容易維護,並減少了錯誤的可能性。這種方法也更接近於我們理解的同步代碼流程,從而使代碼更容易理解和閱讀。
| 特性 | Callback | Promise | Async/Await |
|-----------|------------------|-------------------|-----------------------|
| 概念 | 函數作為參數 | 異步操作的對象 | 異步操作的語法糖 |
| 避免回調地獄 | 否 | 是 | 是 |
| 錯誤處理 | 通過參數或外部處理 | 使用 `catch` 方法 | 使用 `try/catch` 語句 |
| 可讀性 | 較低 | 較高 | 最高 |
#### 面試問題及解答
**問題**:Async/Await 相比於 Promise 有什麼優勢?
**解答**:Async/Await 是基於 Promise 的語法糖,它提供了一種更直觀、更易於閱讀和維護的方式來處理異步操作。使用 Async/Await,代碼的結構更接近於傳統的同步代碼,可以用更簡潔的語法實現複雜的異步流程,並且能夠使用標準的 `try/catch` 語句進行錯誤處理,這使得代碼更整潔,減少了錯誤的可能性。此外,Async/Await 支持鏈式調用,使得代碼邏輯更清晰,更容易理解。
### 17. 遞迴搭配async await? 用途是什麼
遞迴搭配 `async/await` 的主要用途是處理異步操作的遞迴函數,特別是當每次遞迴調用都返回一個 Promise 時。這種方式通常用於處理樹狀結構或任何具有遞迴性質的問題,其中每個遞迴步驟都需要等待異步操作完成。
使用 `async/await` 與遞迴的典型情況包括:
1. **文件系統操作**:當你需要遞歸地讀取或操作文件系統中的文件夾和文件時,使用 `async/await` 可以確保當前操作完成後再執行下一個操作。
2. **網絡請求**:在處理具有父子關係的數據時,例如處理分類目錄,可以使用遞迴和 `async/await` 來處理每個子分類的請求,等待每個請求完成後再處理下一個。
3. **數據庫操作**:當你需要遞歸地查詢或操作數據庫中的數據時,遞迴和 `async/await` 可以幫助你處理這些異步操作。
以下是一個簡單的示例,展示了如何使用 `async/await` 與遞迴來處理文件系統操作,計算文件夾中所有文件的大小:
```javascript=
const fs = require('fs').promises;
const path = require('path');
// 遞迴計算文件夾大小的函數
async function calculateFolderSize(folderPath) {
try {
// 讀取文件夾內的所有文件和子文件夾
const files = await fs.readdir(folderPath);
let totalSize = 0;
// 迭代處理每個文件和子文件夾
for (const file of files) {
const filePath = path.join(folderPath, file);
const stats = await fs.stat(filePath);
if (stats.isFile()) {
// 如果是文件,則將其大小加到總大小
totalSize += stats.size;
} else if (stats.isDirectory()) {
// 如果是文件夾,遞迴調用 calculateFolderSize 函數
totalSize += await calculateFolderSize(filePath);
}
}
// 返回計算出的總大小
return totalSize;
} catch (error) {
// 處理錯誤情況,並返回 0
console.error('Error calculating folder size:', error);
return 0;
}
}
// 主函數
(async () => {
const folderPath = '/path/to/your/folder'; // 指定要計算大小的文件夾路徑
const totalSize = await calculateFolderSize(folderPath); // 調用計算函數
console.log(`Total folder size: ${totalSize} bytes`); // 輸出計算結果
})();
```
這個示例中,`calculateFolderSize` 函數使用遞迴調用和 `async/await` 來計算文件夾的總大小,確保每個異步操作都等待完成後再繼續遞迴。這樣可以有效地處理大型文件系統結構而不會阻塞事件循環。
### 18. Array.forEach、Array.map、Array.filter、Array.reduce 的差別?
在 JavaScript 中,`Array.forEach`, `Array.map`, `Array.filter`, 和 `Array.reduce` 都是對數組進行操作的常用方法,每個都有特定的用途和行為。
#### 1. `Array.forEach`
- **用途**:遍歷數組的每個元素,執行給定的函數,但不返回值。
- **場景**:當你想對數組的每個元素執行操作,但不需要結果集時使用。
**範例**:
```javascript=
const array = [1, 2, 3];
array.forEach(item => console.log(item * 2));
// 輸出: 2, 4, 6
```
#### 2. `Array.map`
- **用途**:創建一個新數組,其元素是對原數組元素調用函數的結果。
- **場景**:當你想基於原數組創建一個新數組時使用。
**範例**:
```javascript=
const array = [1, 2, 3];
const doubled = array.map(item => item * 2);
console.log(doubled); // 輸出: [2, 4, 6]
```
#### 3. `Array.filter`
- **用途**:創建一個新數組,包含通過測試(函數返回 true)的所有元素。
- **場景**:當你想從數組中選擇符合特定條件的元素時使用。
**範例**:
```javascript=
const array = [1, 2, 3, 4, 5];
const even = array.filter(item => item % 2 === 0);
console.log(even); // 輸出: [2, 4]
```
#### 4. `Array.reduce`
- **用途**:對數組中的每個元素執行一個 reducer 函數(接受四個參數:累積器、當前值、當前索引、源數組),將其結果匯總為單個返回值。
- **場景**:當你想對數組中的所有元素進行累積計算時使用。
**範例**:
```javascript=
const array = [1, 2, 3, 4];
// 使用 reduce 函數計算數組中所有元素的總和
const sum = array.reduce((accumulator, currentValue) => {
// `accumulator` 是累積器,它存儲了每次計算的結果。
// `currentValue` 是當前處理的數組元素的值。
// 在這個函數中,我們將累積器 (`accumulator`) 與當前值 (`currentValue`) 相加,
// 並將結果設置為新的累積器的值。這樣,reduce 函數將遍歷數組中的每個元素,
// 將它們的值依次添加到累積器中,最終得到總和。
return accumulator + currentValue;
}, 0); // 0 是 reduce 函數的初始值,它作為累積器的起始值。
console.log(sum); // 輸出: 10
```
#### 比較表
| 方法 | 返回值 | 修改原數組 | 描述 |
|------------|-------|--------|--------------------------|
| `forEach` | 無 | 否 | 僅遍歷數組 |
| `map` | 新數組 | 否 | 創建一個由原數組每個元素調用函數後的新數組 |
| `filter` | 新數組 | 否 | 創建一個包含通過特定測試的元素的新數組 |
| `reduce` | 單一值 | 否 | 通過累積器函數將數組元素累加成單一值 |
#### 面試問題及解答
**問題**:`Array.map` 和 `Array.forEach` 有什麼主要區別?
**解答**:`Array.map` 和 `Array.forEach` 都用於遍歷數組,但主要區別在於 `Array.map` 返回一個新的數組,其中包含對原數組中每個元素調用給定函數的結果,而 `Array.forEach` 僅遍歷數組執行給定操作,不返回任何值。換句話說,`map` 用於數據轉換,forEach 用於執行副作用。
### 19. 如果要實現myMap()跟map的功能一樣,要怎麼做([1,2,3].myMap(()=>{...}))
要實現自己的 `myMap` 函數,可以通過擴展 JavaScript 的 `Array` 原型(prototype)來添加一個自定義的 `myMap` 方法。這個方法接受一個回調函數,並對數組的每個元素應用這個回調函數,最終返回處理後的新數組。
下面是一個實現的程式範例:
```javascript
// 定義自定義的 myMap 方法,它擴展了 Array 的原型
Array.prototype.myMap = function(callback) {
// 創建一個新的空數組,用於存儲處理後的元素
const newArray = [];
// 使用 for 循環遍歷原始數組的每個元素
for (let i = 0; i < this.length; i++) {
// 對每個元素應用回調函數,並將結果添加到新數組中
newArray.push(callback(this[i], i, this));
}
// 返回處理後的新數組
return newArray;
};
// 使用自定義的 myMap 方法
const originalArray = [1, 2, 3];
const doubledArray = originalArray.myMap((element) => element * 2);
console.log(doubledArray); // 輸出: [2, 4, 6]
```
在這個例子中,我們定義了 `myMap` 方法,它接受一個回調函數,然後遍歷原始數組的每個元素,對每個元素應用回調函數,並將處理後的元素添加到新數組中。最終,它返回處理後的新數組。
這是一個常見的 JavaScript 面試問題,通常會涉及到對數組的操作和 JavaScript 的原型(prototype)擴展。其他類似的問題可能包括自定義 `myFilter`、`myReduce` 或其他數組方法的實現。
### 20. Event Bubbling vs. Event Captureing
JavaScript 中的事件傳遞機制包含兩個主要階段:事件捕獲(Event Capturing)和事件冒泡(Event Bubbling)。這兩階段共同構成了 DOM 事件傳播的完整過程。
#### Event Capturing(事件捕獲)
- **階段描述**:當一個事件被觸發時,它首先在文檔的根元素上被捕獲,然後逐級向下傳遞至目標元素(即觸發事件的元素)。
- **啟用方式**:在 `addEventListener` 函數中,將第三個參數設置為 `true`。
- **用途**:處理事件在達到目標元素之前的行為。
#### Event Bubbling(事件冒泡)
- **階段描述**:事件達到目標元素後,開始從目標元素向上冒泡,一直傳播到文檔的根元素。
- **默認行為**:在大多數情況下,事件監聽默認發生在這個階段。
- **用途**:常用於事件代理,可以在父元素上集中處理多個子元素的事件。
#### 程式範例
以下 HTML 和 JavaScript 代碼示例展示了事件捕獲和事件冒泡的過程:
```html=
<!DOCTYPE html>
<html>
<body>
<div id="parent" style="width:200px; height:200px; background-color:lightblue;">
父元素
<button id="child">子元素</button>
</div>
<script>
// 事件捕獲
document.getElementById("parent").addEventListener("click", function() {
alert("事件捕獲:父元素被點擊");
}, true);
// 事件冒泡
document.getElementById("parent").addEventListener("click", function() {
alert("事件冒泡:父元素被點擊");
}, false);
document.getElementById("child").addEventListener("click", function() {
alert("子元素被點擊");
}, false); // 這裡預設為事件冒泡
</script>
</body>
</html>
```
以下是預期輸出結果
```=
--按下 "子元素" 按鈕時的控制台輸出--
事件捕獲:父元素被點擊
子元素被點擊
事件冒泡:父元素被點擊
---
--按下 "父元素"(但不是子元素)區域時的控制台輸出--
事件捕獲:父元素被點擊
事件冒泡:父元素被點擊
```
#### 比較表
| 特性 | 事件捕獲 | 事件冒泡 |
|-----------|--------------|--------------|
| 過程方向 | 從根元素向下到目標元素 | 從目標元素向上到根元素 |
| 啟用方式 | `addEventListener(..., true)` | `addEventListener(..., false)` |
| 主要用途 | 提前攔截事件 | 事件代理、簡化事件處理 |
#### 常見面試問題及解答
1. **問題**:解釋 JavaScript 中的事件委派是什麼。
**解答**:事件委派是一種事件處理模式,它利用了事件冒泡的機制。在這種模式下,代替在每個子元素上單獨設置事件監聽器,我們在它們的共同父元素上設置一個事件監聽器。這樣,當子元素上的事件被觸發並冒泡到父元素時,可以在父元素的事件監聽器中捕獲並處理這些事件。
2. **問題**:如何在事件處理函數中判斷事件是否正在冒泡或捕獲階段?
**解答**:可以使用 `event.eventPhase` 屬性來判斷。這個屬性返回一個數字,表示事件處理的當前階段:1 表示捕獲階段,2 表示目標階段,3 表示冒泡階段。
3. **問題**:如何阻止事件冒泡?
**解答**:可以在事件處理函數中調用 `event.stopPropagation()` 方法來阻止事件繼續冒泡。這會防止事件從當前元素向上傳播到父元素。
4. **事件捕獲和事件冒泡的不同:**
- **事件捕獲**:事件捕獲階段是事件傳播的第一個階段,它從文檔的根節點向下傳播到目標元素。換句話說,事件首先由最外層的祖先元素(通常是 `document` 或 `window`)捕獲,然後逐級向下,直到達到事件的目標元素。
- **事件冒泡**:事件冒泡階段是事件傳播的第二個階段,它從事件的目標元素開始,然後向上冒泡到文檔的根節點。換句話說,事件首先在目標元素上處理,然後逐級向上冒泡到更高級的祖先元素。
5. **問題**:如何停止事件冒泡
- 在 JavaScript 中,可以使用 `event.stopPropagation()` 方法停止事件冒泡。當該方法被調用時,事件不再向上冒泡,並且不會觸發更高層次的元素上的相同事件。
6. **問題**:事件委託(Event Delegation)
- 事件委託是一種常見的技巧,它利用了事件冒泡的特性。通過將事件處理程序綁定到父元素(通常是包含一組子元素的容器),我們可以捕獲到子元素上觸發的事件。這樣做的好處是,我們無需為每個子元素都添加事件處理程序,而是僅需在父元素上添加一個事件處理程序,從而節省了代碼和性能。同時,它也適用於動態添加或刪除的子元素。
7. **問題**:event.target 和 event.currentTarget 的區別
- `event.target`:這是觸發事件的元素,即事件的實際目標。在事件冒泡階段中,它始終指向觸發事件的元素。
- `event.currentTarget`:這是當前正在處理事件的元素,即事件處理程序附加的元素。在事件冒泡階段中,它始終指向綁定事件處理程序的元素。
### 21. 請解釋「淺拷貝」和「深拷貝」
當然可以。使用簡單的數組(Array)和整數(int)來解釋「淺拷貝」和「深拷貝」可以使這個概念更加直觀。在 JavaScript 中,基本數據類型(如數字、字符串)是按值傳遞的,而對象和數組是按引用傳遞的。這是理解淺拷貝和深拷貝之間差異的關鍵。
#### 淺拷貝
淺拷貝創建了對象或數組的第一層副本。如果對象中包含基本數據類型,它們將被直接複製。如果對象包含其他對象或數組,則這些內部對象或數組的引用將被複製,而不是內容本身。
**範例**
```javascript=
let originalArray = [1, 2, [3, 4]];
let shallowCopy = originalArray.slice(); // 創建一個淺拷貝
// 修改原始數組中的嵌套數組
originalArray[2].push(5);
console.log(shallowCopy); // 輸出: [1, 2, [3, 4, 5]]
```
在這個範例中,`shallowCopy` 是 `originalArray` 的淺拷貝。修改 `originalArray` 中的嵌套數組會影響 `shallowCopy`,因為嵌套數組的引用在兩個數組中是共享的。
#### 深拷貝
深拷貝創建了對象或數組的完整副本,包括所有嵌套的對象和數組。這意味著原始對象和副本之間不會共享任何內容。
**範例**
```javascript=
let originalArray = [1, 2, [3, 4]];
let deepCopy = JSON.parse(JSON.stringify(originalArray)); // 創建一個深拷貝
// 修改原始數組中的嵌套數組
originalArray[2].push(5);
console.log(deepCopy); // 輸出: [1, 2, [3, 4]]
```
在這個範例中,`deepCopy` 是 `originalArray` 的深拷貝。即使原始數組被修改,`deepCopy` 也不會受到影響,因為所有內容都被完全獨立地複製了。
#### 比較表
| 類型 | 描述 | 基本類型影響 | 嵌套對象/數組影響 |
|------|---------------------|--------------|------------------|
| 淺拷貝 | 僅複製第一層 | 直接複製 | 引用共享 |
| 深拷貝 | 完全複製所有層次 | 直接複製 | 完全獨立複製 |
在處理僅包含基本數據類型(如整數)的對象時,淺拷貝和深拷貝之間的差異不顯著,因為基本類型總是被直接複製。然而,當對象包含嵌套的對象或數組時,這兩種拷貝方法之間的差異就變得重要了。