BACK
Featured image of post 前端面試常考的 JavaScript 手寫題彙整

前端面試常考的 JavaScript 手寫題彙整

前端手寫題是許多面試會考的題型,從最基本上的各類效用函式(例如 debounce、curry、deepClone 等等),到常見的 JavaScript 方法(例如 Promise.all),都不能只會用,還要會自己手寫出來。本篇文章將會節錄常見的手寫題目,並附上解答。除此之外,每一題都有詳細解說,供讀者們參考。

參考網站

前端手寫題是許多面試會考的題型,從最基本上的各類效用函式 (例如 debouncecurrydeepClone 等等),到常見的 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)**來優化。

防抖函式會接受兩個參數:

  • 延遲的時間(ms)
  • 要執行的函式

以上面搜尋框的例子來說,我們透過防抖就可以完成:當使用者停止在搜尋框內打入文字超過一定的時間,此時才會去執行觸發 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 這個套件提供的效用函式舉例,也是分成 clonecloneDeep 兩種不同效用函式,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 反序列化轉換回物件。

要特別注意,這做法只能用於可序列化的物件,有些無法序列化的物件例如:functionHTML 的元素,這些是無法序列化的!所以執行前,需要先確認是否可以序列化,否則在執行 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]

comments powered by Disqus