參考網站
前端手寫題是許多面試會考的題型,從最基本上的各類效用函式 (例如 debounce
、curry
、deepClone
等等),到常見的 JavaScript 方法 (例如 Promise.all
),都不能只會用,還要會自己手寫出來。
本篇文章將會節錄常見的手寫題目,並附上解答。除此之外,每一題都有詳細解說,供讀者們參考。
Promise.all
Promise.all() 是什麼?
要實現這個方法前,我們要先知道它在做什麼。根據 MDN 的定義,Promise.all()
會:
- 接收一個內有多個 promises 的
Iterable
,例如 Array、Map、Set。 - 如果
Iterable
是空的,例如空 Array,則 fulfilled 值會是空的 Array。 - 如果
Iterable
不是空的,則如果所有的 promises 都 fulfilled,則依序回傳 fulfilled 的值;如果其中有一個 promise 被 rejected,則會馬上 reject。
1
2
3
4
5
6
7
8
9
10
| const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
|
如何實現簡單版的 Promise.all()
在實現完整版之前,我們先以一個僅處理 Array 的 Promise.all
為例子,了解如何實現核心概念,往下再進一步探討如何處理 Iterable
。我們先直接看程式碼,看看你能了解多少。有不懂的地方也不擔心,下面會透過註解,一行行解釋:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| function promiseAll(promises) {
if (!Array.isArray(promises)) {
return new TypeError('Arguments must be an array');
}
if (promises.length === 0) {
return Promise.resolve([]);
}
const outputs = [];
let resolveCounter = 0;
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
promise
.then((value) => {
outputs[index] = value;
resolveCounter += 1;
if (resolveCounter === promises.length) {
resolve(outputs);
}
})
.catch(reject);
});
});
}
|
讓我們透過以下程式碼與註解來看如何實現吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| function promiseAll(promises) {
// 先檢查輸入是不是 array,如果不是的話就回傳錯誤
if (!Array.isArray(promises)) {
return new TypeError('Arguments must be an array');
}
// 定義中有提到,如果輸入是空的,例如空 array,就 resolve 一個空 array
if (promises.length === 0) {
return Promise.resolve([]);
}
// 先宣告一個最終要 resolve 的 outputs,之後每個 promise 被 fulfilled 時,就放到 outputs 裡面
const outputs = [];
// 我們需要這個 counter 讓我們知道有多少個 promise 已經 fulfilled
let resolveCounter = 0;
// Promise.all() 最終要回傳一個 promise
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
promise
.then((value) => {
// 當輸入的每個 promise 成功 fulfilled 時,就放到 outputs
// 透過 index,我們可以確保順序正確
outputs[index] = value;
// 每次成功放入時,counter 要加一
resolveCounter += 1;
// 當 counter 等於 promises 的長度時,代表所有的 promise 都 fulfilled
// 這時最外面的 promise 就可以 resolve
if (resolveCounter === promises.length) {
resolve(outputs);
}
})
.catch(reject); // 如果有任何一個 reject,就直接 reject
});
});
}
|
實現完整版的 Promise.all()
上面這個版本的 Promise.all()
只有處理 Array 這種輸入,但實際上的 Promise.all()
是能接收所有的 Iterable
,因此我們可以進一步優化上面的版本(備註:在面試中能寫出上面的版本,基本上要過關是沒問題,當然如果想在面試中展現自己的細心度,那麼進一步優化是更好的選擇)。
先想想,如果要處理任意的 Iterable
可以怎麼做?我們可以先判斷丟進來的輸入是不是可以迭代的,如果不是的話,就提早回傳錯誤。
1
2
3
4
5
6
7
| const isIterable =
((typeof promises === 'object' && promises !== null) || typeof promises === 'string') &&
typeof promises[Symbol.iterator] === 'function';
if (!isIterable) {
return new TypeError('Arguments must be iterable');
}
|
基本上多了上述步驟的處理,剩下的邏輯就跟 Array 版本的差不多,程式碼如下(不同之處會有註解):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| function promiseAll(promises) {
// 判斷輸入是否為 Iterable
const isIterable =
((typeof promises === 'object' && promises !== null) || typeof promises === 'string') &&
typeof promises[Symbol.iterator] === 'function';
// 不是的話就回傳錯誤訊息
if (!isIterable) {
return new TypeError('Arguments must be iterable');
}
// 把 Iterable 轉成 Array,就可以重複用 Array 版的邏輯
promises = Array.from(promises);
if (promises.length === 0) {
return Promise.resolve([]);
}
const outputs = [];
let resolveCounter = 0;
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
promise
.then((value) => {
outputs[index] = value;
resolveCounter += 1;
if (resolveCounter === promises.length) {
resolve(outputs);
}
})
.catch((e) => {
// 或是只要有 reject 的,就馬上 reject
reject(e);
});
});
});
}
|
Promise.race()
Promise.race() 是什麼?
要實現這個方法前,我們要先知道它在做什麼。根據 MDN 的定義,Promise.race()
會
- 接收一個內有多個 promises 的
Iterable
,例如 Array、Map、Set - 回傳最先 fulfill 或最先被 reject 的那一個
如何實現 Promise.race()
我們先直接看程式碼,看看你能了解多少。有不懂的地方也不擔心,下面會透過註解,一行行解釋:
1
2
3
4
5
6
7
8
9
10
11
| function promiseRace(promises) {
return new Promise((resolve, reject) => {
for (const p of promises) {
p.then((val) => {
resolve(val);
}).catch((e) => {
reject(e);
});
}
});
}
|
讓我們透過以下程式碼與註解來看如何實現吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| function promiseRace(promises) {
return new Promise((resolve, reject) => {
// 迭代過 promises
for (const p of promises) {
p.then((val) => {
// 只要有 fulfill 的,就馬上 resolve
resolve(val);
}).catch((e) => {
// 或是只要有 reject 的,就馬上 reject
reject(e);
});
}
});
}
|
Lodash 的 .get()
Lodash 的 .get() 在做什麼?
.get()
是很常被用到的一個效用函式,他做的事情是給定一個物件,以及某個路徑,要回傳此路徑的值;如果該路徑不存在於給定的物件,則返回預設值。透過例子會比較好理解 (以下例子來自 Lodash 原始碼)
1
2
3
4
5
6
7
8
9
10
| const object = { a: [{ b: { c: 3 } }] };
//=> 3
get(object, 'a[0].b.c');
//=> 3
get(object, 'a[0]["b"]["c"]');
//=> 'default
get(object, 'a[100].b.c', 'default');
|
從上面的例子可以看到 Lodash 這個函式庫,可以接收各種類型的路徑,主要是原始碼當中有 castPath 這個 helper function。castPath
會把上面的 "a[0].b.c"
、'a[0]["b"]["c"]'
以及 "a[100].b.c"
都轉成好處理的 ['a', '0', 'b', 'c']
。
因此,如果給定有內建的 castPath
,.get()
實際在做的事情是:
1
2
3
4
5
| // 給定一個物件,例如
const object = { a: [{ b: { c: 3 } }] };
// 給一個路徑,透過 .get() 找到該路徑的值,例如
get(object, ['a', '0', 'b', 'c']); // 回傳 3
|
如何實現 .get()
一起來看看如何實現 .get()
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| function lodashGet(object, path, defaultValue) {
// 先確認傳進來的 object 不是 null,如果是則直接回傳 defaultValue
if (object == null) {
return defaultValue;
}
let count = 0;
const length = path.length;
// 依循路徑一層層走過該物件,以上面的例子來說,會是
// path[0] 為 'a',所以第一次迴圈 object 會變成 object['a'] 也就是 [{ b: { c: 3 } }]
// 第一次迴圈後,count 加 1,所以變成 object[path[1]]
// 也就是 [object['0']],意即 { b: { c: 3 } }
// 接著 count 再加一,所以 object 會成為 object[path[2]]
// 也就是 object['b'],意即 { c: 3 }
// 接著 count 再加一,所以 object 會成為 object[path[3]]
// 也就是 object['c'],意即 3
// 這時 count 為 4,由於 length 也是 4,因為 4 不小於 4,所以跳出迴圈
while (object != null && count < length) {
object = object[path[count++]];
}
// 因為上面如果 object 是 null 的話,在走完 length 長度前,就會跳出 while 迴圈
// 這種情況下,就代表依循該路徑,會找不到值,所以會是 undefined
// 舉例來說,如果 path 是 ['a', '1', 'b', 'c']
// 因為 object['1'] 會是 undefined,所以這時 while 迴圈會在 count 為 2 時終止
// 這種情況下就會是 count < length,所以當 count < length,result 會是 undefined
const result = count && count == length ? object : undefined;
// 如果 result 是 undefined,代表依循該路徑,會找不到值,所以回傳預設值
// 如果依循路徑有找到值,就回傳 result
return result === undefined ? defaultValue : result;
}
|
防抖函式(debounce)
防抖(debounce) 和節流(throttle) 絕對是在考手寫題時最常出現的前幾名,這兩者都能做到優化,但使用情境不太相同。
防抖(debounce)在做什麼?
防抖函式(debounce)是指,將多次操作優化為:只在最後一次執行。
具體來說,當一定時間內沒有持續觸發事件時,事件處理函式才會被執行一次,但如果在設定的時間內又一次觸發了事件,就會重新開始計時。
在手寫**防抖(debounce)**函式前,我們先來了解防抖的使用情境。
我們以 Google 搜尋框和搜尋建議列表為例子。畫面如下,有一個搜尋框,和一個搜尋建議列表,使用者在此搜尋框輸入文字後,搜尋建議列表會即時呈現結果; 搜尋框的文字只要一改變,搜尋建議列表也會即時更新結果。
這樣看起來是很理想的設計吧?但首先要去解決一個問題:如果使用者一直在搜尋框內書寫文字,這樣會一直觸發 API 去更新搜尋建議列表,例如使用者想搜尋 javascript,這時 API 也及時觸發,所以此 API 會被觸發 10 次,而且前 9 次不會是使用這想要的結果。
為了提升使用者體驗以及優化程式嗎,這一段功能我們就可以透過**防抖(debounce)**來優化。
防抖函式會接受兩個參數:
以上面搜尋框的例子來說,我們透過防抖就可以完成:當使用者停止在搜尋框內打入文字超過一定的時間,此時才會去執行觸發 API 的函式。
如何實現防抖函式(debounce)
我們先直接看程式碼,看看你能了解多少。有不懂的地方也不擔心,下面會透過註解,一行行解釋:
1
2
3
4
5
6
7
8
9
10
| function debounce(fn, delay = 500) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
|
程式碼註解版本和實際應用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| // debounce function 接受兩個參數
// 一是:要執行的 function
// 二是:要延遲的豪秒數,這邊預設 500 毫秒
function debounce(fn, delay = 500) {
let timer;
// debounce function 最終會回傳一個 function
return (...args) => {
// 每一次 debounce function 被觸發時,會先清除之前的 timer,避免觸發先前的 fn 函式
// 因此只要在 delay 時間內觸發 debounce function,就會一直清除先前的 timer,避免 fn 一直被執行
clearTimeout(timer);
// 清除之後,再重新計時
// 當 delay 時間到時,執行 fn
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
// updateDebounceText 會在延遲 500 ms 後執行 console.log('call api get search result')
const updateDebounceText = debounce((text) => {
console.log('call api get search result');
}, 500);
// 搜尋框監聽 input 事件,當 input 改變時
// 觸發 updateDebounceText 函式
searchInput.addEventListener('input', (e) => {
updateDebounceText(e.target.value);
});
|
節流函式(throttle)
節流(throttle)在做什麼?
**節流(throttle)**指的是,在一段時間內只會執行一次觸發事件的回調函式(callback),若在這之中又有新事件觸發,則不執行此回調函式。
在手寫**節流函式(throttle)**前,我們先來了解節流的使用情境。
監聽滾動事件,是搭配節流的使用場景之一。舉例來說:要判斷使用者是否已經滑動到頁面的 30% 處,當到達時會觸發一些動畫效果,因此會透過監聽滾動事件時,計算是否已到達該位置,但如果只要一滾動就計算會非常消耗性能,透過**節流(throttle)**可以將計算的這個回調函式在固定時間內合併一次被執行。
這裡不適合使用防抖的原因是,防抖只會在事件停止被觸發後的一段時間內被執行一次。因此如果用防抖,當使用者一直滑動頁面,函式就永遠不會被觸發。這邊我們仍想要函式在滑動過程中被觸發,只是不想那麼頻繁被觸發,這種情境下,節流就可以派上用場。
**節流函式(throttle)**會接受兩個參數:
- 延遲的時間(ms)
- 要執行的回調函式(callback)
如何實現節流函式(throttle)
我們先直接看程式碼,看看你能了解多少。有不懂的地方也不擔心,下面會透過註解,一行行解釋:
1
2
3
4
5
6
7
8
9
10
11
| function throttle(fn, delay = 500) {
let timer = null;
return (...args) => {
if (timer) return;
timer = setTimeout(() => {
fn(...args);
timer = null;
}, delay);
};
}
|
程式碼註解版本和實際應用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| function throttle(fn, delay = 500) {
let timer = null;
// throttle 本身會回傳一個函式,透過 ...args 拿到該函式的引數
return (...args) => {
// 如果有計時器,表示還在 delay 的秒數內
// 直接 return,不往下執行程式碼
if (timer) return;
// 如果計時器不等於 null,會進到以下邏輯
// 設定計時器,在 delay 秒數之後,會執行回調函式 fn,並將計時器值為改為 null
// 如果還不到 delay 的秒數,則 timer 的值不為 null,不會進到這段邏輯
// 可以達到 throttle 的目的,將一段時間內的操作,集合成一次執行
timer = setTimeout(() => {
fn(...args);
timer = null;
}, delay);
};
}
const updateThrottleText = throttle(() => {
console.log('throttle');
}, 500);
// 如果一直滑動畫面,會固定 500ms console.log('throttle')
window.addEventListener('scroll', () => {
updateThrottleText();
});
|
淺拷貝(shallow copy)與深拷貝(deep copy)
在 JavaScript 複製值時,當複製的是非**原始型別(primitive type)**的資料型別時,例如:物件(object)、**數組(array)**等,會遇到淺拷貝(shallow copy)
和深拷貝(deep copy)
的差異。在面試時被問到這兩者的差異,你會怎麼回答?如果要你當場手寫深拷貝,你會怎麼寫?
比較淺拷貝(shallow copy) 和深拷貝(deep copy)
淺拷貝是指複製值時,原本的變數和新的變數會指向同一個址(reference),換句話說,如果拷貝的物件內容有改變,原本的物件也會被改變。
以下程式碼是一個簡單的例子。
定義一個新變數 objB
,並將其值賦予為 objA
,此時改變 objB
也會同時改變 objA
,因為這兩個變數是共享同一個址(reference)。
1
2
3
4
5
6
7
8
9
10
11
| let objA = {
a: 1,
b: 2,
};
let objB = objA;
objB.a = 3; // 因為是淺拷貝,objA 的 a 也會被改變
console.log(objA); //{a:3, b:2}
console.log(objB); //{a:3, b:2}
|
相反地,深拷貝是指在拷貝時不共享相同的址(reference)。
以上面的例子,如果 objB
是透過深拷貝創建出來,當我們在更改 objB
的值時,並不用擔心 objA
的值會同樣被更動到。
補充說明,在拷貝值時,有可能會遇到變數是多層的情境,例如是一個物件裡還有物件,深拷貝的定義會是每一層的值都不會共享址(reference)。以 lodash 這個套件提供的效用函式舉例,也是分成 clone
和 cloneDeep
兩種不同效用函式,clone
只用於淺拷貝(第一層拷貝),但 cloneDeep
可用於深拷貝。
1
2
3
4
5
6
7
8
9
10
11
| // lodash 的淺拷貝 clone
var objects = [{ a: 1 }, { b: 2 }];
var shallow = _.clone(objects);
console.log(objects === shallow); // false
console.log(shallow[0] === objects[0]); // true
// lodash 的深拷貝 cloneDeep
var objects = [{ a: 1 }, { b: 2 }];
var deep = _.cloneDeep(objects);
console.log(objects === deep); // false
console.log(deep[0] === objects[0]); // false
|
在說明完淺拷貝與深拷貝的差別後,常見的接續問題是「手寫」淺拷貝與深拷貝。
假如你不確定怎麼手寫這兩種拷貝方式,可以繼續往下看。
如何實現淺拷貝(shallow copy)
方法一:手動複製值
1
2
3
4
5
6
7
8
9
| let objA = {
a: 1,
b: { c: 3 },
};
let objB = { a: objA.a, b: objA.b };
console.log(objA === objB); // false
console.log(objA.b === objB.b); // true, 第二層的物件還是指向相同位置
|
方法二:使用 spread syntax
1
2
3
4
5
6
7
8
9
| let objA = {
a: 1,
b: { c: 3 },
};
let objB = { ...objA };
console.log(objA === objB); // false
console.log(objA.b === objB.b); // true, 第二層的物件還是指向相同位置
|
方法三:使用 Object.assign
1
2
3
4
5
6
7
8
9
| let objA = {
a: 1,
b: { c: 3 },
};
let objB = Object.assign({}, objA);
console.log(objA === objB); // false
console.log(objA.b === objB.b); // true, 第二層的物件還是指向相同位置
|
如何實現深拷貝(deep copy)
方法一:使用 JSON.parse(JSON.stringify(...))
這個作法是先將物件用 JSON.stringify
序列化為 string,再透過 JSON.parse
反序列化轉換回物件。
要特別注意,這做法只能用於可序列化的物件,有些無法序列化的物件例如:function、HTML 的元素,這些是無法序列化的!所以執行前,需要先確認是否可以序列化,否則在執行 JSON.stringify
時會失敗。
1
2
3
4
5
6
7
8
9
10
11
12
13
| let objA = {
a: 1,
b: { c: 3 },
};
function deepCopy(item) {
return JSON.parse(JSON.stringify(item));
}
let objB = deepCopy(objA);
console.log(objA === objB); // false
console.log(objA.b === objB.b); // false
|
方法二:使用 structuredClone(value)
針對可序列化的物件,有另外一種透過 JavaScript 內建的方法達成深拷貝。這種方法是 structuredClone(value)
,用法如下:
1
2
3
4
5
6
7
8
9
| let objA = {
a: 1,
b: { c: 3 },
};
let objB = structuredClone(objA);
console.log(objA === objB); // false
console.log(objA.b === objB.b); // false
|
方法三:考慮多重情況的遞迴式深拷貝
通常在面試中,用上述兩種方式,可能會被面試官追問說,如果不用這種現成的方法,要如何手寫?
以下的寫法是透過遞迴的方式,來進行深拷貝:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| function deepClone(obj) {
// 確認是不是原始型態,如果是的話就直接回傳
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 確認是不是 Date 或 RegExp 這種特殊型態,是的話就透過建構式複製一個相同的值,然後回傳
if (obj instanceof Date || obj instanceof RegExp) return obj.constructor(obj);
// 看 obj 是物件還是陣列,然後先建一個新的空物件 (或空陣列)
let result = Array.isArray(obj) ? [] : {};
// 透過 Ojbect.entries 來迭代,然後遞迴地對每個值深拷貝
// 因為 Object.entries 不會列舉整個原型鍊 (prototype chain)
// 所以不用透過 obj.hasOwnProperty(key) 額外檢查是不是非原型鏈上的屬性
for (const [key, value] of Object.entries(obj)) {
result[key] = deepClone(value);
}
// 最後回傳結果
return result;
}
|
陣列去除重複(remove duplicates)方法
在陣列中把重複的項目去除掉,是很常在實際工作中需要執行的操作。因此在 JavaScript 面試時,也很常會被問。這種題目通常會要當場手寫,所以假如你不太確定要怎麼手寫去除重複,一定要在面試前多練習幾次。
陣列去除重複題目長什麼樣?
陣列去除重複的面試題,通常會像下面這樣,給一個陣列,裡面有重複的數字,並要求寫一個 removeDuplicate
函式,輸入是原始陣列,輸出是去除重複數字的陣列。
1
2
3
4
5
6
7
8
| const originalArr = [9, 1, 2, 2, 3, 4, 2, 4, 8, 1, 9]
function removeDuplicate(array){
// ...
}
const ans = removeDuplicate(originalArr);
console.log(ans) // [9, 1, 2, 3, 4, 8]
|
陣列去重複其實有很多種做法,以下會列出 4 種常見做法。在面試考手寫題時,陣列去重複有可能是單獨一種題型,或者是在解手寫題時一開始需要將陣列進行去重複的操作。
建議詢問面試官題目的需求、主動跟面試官討論期望的解法,再選擇要使用哪一種解法。
四種解法
解法一:使用 Set
去重複
Set
的資料格式與用法類似於 Array
,但有一特色是 Set
中只能儲存任何資料的唯一值,因此可以先將 Array
轉為 Set
,此時重複的值會被移除,再將 Set
轉為 Array
。程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
| function removeDuplicate(arr) {
return Array.from(new Set(arr));
}
// 也可以利用 spread syntax 更簡化程式碼
// function removeDuplicate(arr) {
// return [...new Set(arr)];
// }
let arr = [1, 2, 3, 2, 3, 8];
let arrAfter = removeDuplicate(arr);
console.log(arr1After); // [1, 2, 3, 8]
|
解法二: filter
搭配 indexOf
此解法先用 array 的 filter 方法,搭配 indexOf 方法,只保留第一次出現的值,所以只要是第二次出現的,就會被篩掉,這能確保結果不會有重複的。程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
| function removeDuplicate(arr) {
// indexOf 會回傳在這個 array 等同於此值第一個 item 的 index,
// 所以如果 indexOf 回傳的 index 相等於目前 filter 到的值,
// 則代表該值是第一次出現,我們保留起來,
// 反之,如果 index 不等於,則代表此 array 中前面位置已經出現過,所以就 filter 掉。
return arr.filter((item, index, array) => array.indexOf(item) === index);
}
let arr = [1, 2, 3, 2, 3, 8];
let arrAfter = removeDuplicate(arr);
console.log(arrAfter); // [1, 2, 3, 8]
|
解法三: 雙層 for loop
雙層 for loop 是一種暴力解。依序遍歷整個 array,再透過第二層 for loop 找出重複的值將其移除。程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| function removeDuplicate(arr) {
// 第一層 for loop,i 從 index 0 開始,到 arr 最後
for (let i = 0, len = arr.length; i < len; i++) {
// 第二層 for loop,j 從 i + 1 開始,要檢查值是否重複
for (let j = i + 1; j < len; j++) {
// 如果值重複,則透過 splice 方法將 j 位置的值從 arr 去除
if (arr[i] == arr[j]) {
arr.splice(j, 1);
// 移除重複的值之後,arr length 長度會需要減 1
len--;
// j 位置的值被移除,因此 j index 也要減 1
j--;
}
}
}
return arr;
}
let arr = [1, 2, 3, 2, 3, 8];
let arrAfter = removeDuplicate(arr);
console.log(arrAfter); // [1, 2, 3, 8]
|
解法四: 透過 object 或 Map 儲存以遍歷過的項目
我們可以透過 object 或 Map 來儲存已經遍歷過的項目,來找出是否已存在陣列當中,如果還不在,那就放進要輸出的陣列;如果已經在了,就不再放入,這樣一來能確保陣列中沒有重複的值。程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| function removeDuplicate(arr) {
let seen = {};
let newArray = [];
// 遍歷過原本的陣列
for (let item of arr) {
// 判斷當前被遍歷到的項目是否已經放入過
if (seen[item] !== true) {
newArray.push(item); // 如果還沒,則放入要被輸出的新陣列
seen[item] = true; // 這時紀錄一下這個項目已經被放入,下次就不會再被放入
}
}
return newArray;
}
let arr = [1, 2, 3, 2, 3, 8];
let arrAfter = removeDuplicate(arr);
console.log(arrAfter); // [1, 2, 3, 8]
|