# Javascript 常見操作簡介

由於篇幅的關係,這裡只介紹常見的陣列操作。

# 陣列的操作

map filter reduce every some forEach

為什麼陣列的操作那麼重要?雖然沒有這些方法你也可以做開發,以及做到完全一樣的事情,但是這些高階操作能夠幫助你、以及你的夥伴更容易清楚你的程式碼在做什麼。

最主要的差別在於聲明式與命令式。舉例來說,如果我們想要反轉一個字串:

// 命令式
var str = "abcd";
var result = "";
for (let i = str.length -1; i >= 0; i--) {
  result += str[i];
}

// 聲明式
"abcd".reverse();

可以發現幾件事情:

  • 與命令式比起來,少了變數宣告
  • 與命令式比起來,少了 for...loop 迴圈

這看起來有點像...把實作包裝成函數?

可以這麼說,但怎麼設計這個函數,讓他提供足夠的抽象,同時又能具備一定程度的彈性,是在寫聲明式時要注意的事情。

這樣寫可以更有效幫助我們 debug,也更容易表達出這段程式碼想要做什麼事情。或許有人會懷疑會不會讓效能變差,不過除非你的程式已經到需要這樣斤斤計較的地步,不然犧牲一點點的效能,可以換來易讀性更高的程式碼。

以下介紹一些陣列當中常見的操作:

# map(function(val, index, arr))

能夠將陣列轉換成新的陣列。

# 基本範例:

// 將數字 * 2
const arr = [1,2,3,4];
arr.map(val => val * 2);
function multipy(n) {
  return (val) => {
    return val * n;
  }
}
arr.map(multipy(2));

// 轉換 data field
const data = [
  { Name: 'kalan', Age: 22, Introduction: { Brief: 'xxxx', Details: 'longlongtext'}},
    { Name: 'kalan2', Age: 22, Introduction: { Brief: 'xxxx2', Details: 'longlongtext2'}}
];

data.map(user => {
  return {
    name: user.Name,
    age: user.Age,
    introduction: user.Introduction.Brief,
  };
});

我們看到第一個案例,簡單地將所有數字 * 2。另外一個比較實用的案例是,我們將回傳的資料先轉換成小寫,並取出我們想要的欄位,轉換後的資料比較容易拿來使用。比起用 for(let i = 0; i < arr.length; i++) 程式碼更簡潔,程式碼要表達的意圖也相當明顯。

除非你在做演算法相關的試題或應用,用迴圈比較好處理,或是你正在拼命榨出效能,不然處理資料的部分通常可以用 map 等高階操作搞定。

特別要注意的是,因為這個函數大家期待的是一個新的 array,所以強烈建議不要在裡頭做任何 side effect 的操作,像是 console.log,呼叫 API,更改 DOM 等等,因為大家並不預期裡頭會有這樣的操作,就算你相當清楚你在做什麼,過了一個禮拜後你還是有可能忘記,並且埋下了一個或許不好發現的 bug。

# filter

filter 這個 API 命名其實有點不明確,因為有時常常會想,到底是把符合條件的值 filter 掉,還是把符合條件的值留下來?

在做資料查詢,或是你想要根據某個條件過濾值的時候相當好用:

const arr = [1,2,3,4,5];
arr.filter(val => val % 2 === 0); // [2,4];

// 挑出已成年的人
const data = [
  { id: 1, age: 18},
  { id: 2, age: 20},
  { id: 3, age: 14},
];

data.filter(val => val.age >= 18);

# reduce(function(accumalator, current, index, array), initialValue)

reduce 的妙用相當多,最常見的就是將陣列組合成一個值,例如計算總和:

const arr = [1,2,3,4,5];
arr.reduce((acc, curr) => {
  acc += curr;
  return acc;
}, 0)

或是把陣列全部塞到物件裡頭:

const arr = [
  {id: 'a', name: 'wu' },
  {id: 'b', name: 'ka'},
  {id: 'c', name: 'jack'},
];
const result = arr.reduce((acc, curr) => {
  return Object.assign(acc, {
    [curr.id]: curr
  });
}, {});
console.log(result)
// {a: {id: "a", name: "wu"}
// b: {id: "b", name: "ka"}
// c: {id: "c", name: "jack"}}

# every(function(val, index))

如同它的名字 every 一樣,會根據回傳值,來判定只有當陣列全部的值都為 true 才回傳 true。例如我想要這個陣列裡頭全部都是 18 歲以上的人,就可以這樣寫:

const people = [
  { age: 18 },
  { age: 20 },
  { age: 22 },  
];

const isAllAdult = people.every(p => p.age >= 18); // true

# some

和 every 類似的函數,不過只要其中一個元素符合傳入的函式就會回傳 true。

const messages = [
  'hello',
  'hello world',
  'apple'
];

const containHello = messages.some(msg => msg.includes('hello')); // true

# 小結

為什麼會特別介紹這些 API 是因為剛開始為了學習方便會習慣用 for loop 的方式寫,如果沒有常常去看其他人的程式碼或是和別人合作過,難免就這樣繼續寫下去了,但是在處理資料或是寫應用的時候,寫這些程式碼不只會多出好幾個變數,閱讀起來也不是那麼方便,往往要看到迴圈內的操作才能明白想要幹嘛,因此或許已經有很多人都知道,但還是花了一個章節來談論它。

除此之外,在 JavaScript 以及 ES6 語法當中,也有許多可以讓 code 看起來更簡潔的小技巧,這裡就不多贅述。

雖然 JavaScript 中有很多奇技淫巧,但正常的情況下你應該避免使用它。我們不應該為了少寫一點點的程式碼而選用一個晦澀難懂的方式。任何語言都是這樣。

# 進階篇 — 實作 Array.prototype.map

了解陣列的操作後,我們試著從設計 API 角度出發,實作看看 map。

根據 MDN (opens new window) 上的描述,一個 map 的函式簽名是這樣的:

var new_array = arr.map(function callback(currentValue[, index[, array]]) {
    // Return element for new_array
}[, thisArg])

第一個參數是一個函數,回傳的值將會成為陣列的新元素,第二個可選參數是 this,你可以綁定自己的 this。

首先要思考的是,如果用 prototype 來實作 map 這個 function,那麼 this 會是什麼?在 Array.prototype 中的話,this 就會是整個陣列:

const arr = [];
arr.map()
Array.prototype.map = function(fn, thisArg) {
  const result = [];
  for (let i = 0; i < this.length; i++) {
	  result.push(fn.call(thisArg || null), this[i], i, this));
  }
  return result;
};

一個簡單的函式實作可以看到 API 設計時可以考量的地方:

  • 透過傳入 function 將實作交給外部決定,不會讓函數過度耦合
  • 盡可能地將參數提供出來給實作者操作,像是 currentValue, index, array 等等
  • 透過 thisArg 的綁定傳入特定的 context。

在建立函數的時候也有煩惱不知道要參數化哪些地方嗎?試著從原生的 API 中思考一下吧!