本篇介紹的陣列方法將寫程式的焦點從「如何做」轉移到「做什麼」,也就是 functional programming 中的「宣告性編程」。這些陣列方法易學易用,能夠幫助你提升程式碼的可維護性並減少潛在的bug,所以我認為每個 JavaScript 的使用者都有必要了解本篇介紹的陣列方法。

專注在「做什麼」,而非「如何做」

假設我們有一組商品資料:

const data = [
  {
    name: '蘋果',
    price: 100,
  }, {
    name: '香蕉',
    price: 200,
  }, {
    name: '橘子',
    price: 300,
  },
];

我們可以對這組資料做各式各樣的操作,如果我想要所有品項「價格」的陣列,可以這樣寫:

const results = [];
for (let i = 0; i < data.length; ++i) {
  results.push(data[i].price);
}

console.log(results); // [100, 200, 300]

如果我的需求換成「打完折加上運費的價格」的陣列,那我可以這樣改寫:

const results = [];
for (let i = 0; i < data.length; ++i) {
  results.push(data[i].price * 0.9 + 30);
}
console.log(results); // [120, 210, 300]

注意到上面兩段程式碼有共同的模式,也就是:

  1. 新增一個空陣列
  2. 用一個 for 迴圈遍歷陣列
  3. 經過某種計算後將結果放進陣列中

這些重複使用的邏輯,可以提取成一個新的陣列方法 map()

Array.prototype.map = function(callback) {
  const results = [];
  for (let i = 0; i < this.length; ++i) {
    results.push(callback(this[i]));
  }
  return results;
}

將上述兩個例子用 map() 改寫如下:

const result1 = data.map(item => item.price);
const result2 = data.map(item => item.price * 0.9 + 30);

怎麼樣,是不是簡潔許多呢?

這就是 functional programming 強調的宣告式編程 (Declarative Paradigm)。

宣告式編程指的是,寫程式的時候專注在「我想要達到什麼結果」,而不是「如何達成這個結果」。

這裏的 item => item.price * 0.9 + 30 就是在告訴電腦,我想要這樣的結果,但是結果如何產生的我不需要在意,所有細節都已經封裝在 map() 裡面了。

這樣做的好處是,寫出來的程式碼的「意圖」會變的很明白,讀程式碼的人只需要關注真正重要的邏輯,也就是 item => item.price * 0.9 + 30,而不會迷失在細節中。

JavaScript 內建 Functional Programming

Functional Programming 是一種程式設計的方式,主要的特色是函式是第一等物件、提倡純函式 (Pure Function) 及避免副作用 (Side Effect)、宣告式 (Declarative) 程式設計等。

實際上 JavaScript 中的 Array 內建包含 map() 在內的許多方法,讓我們可以很方便地以 functional programming 的方式操作資料。

接下來就要介紹這些方法:forEach()map()filter()find()every()some()reduce()

forEach()

forEach 可以對陣列中的所有元素執行特定操作。

forEach 接受一個 callback 函式當作參數,用來定義對每個元素要做的事情:

arr.forEach(function(item, index, array) {
  // Do something
});

callback 的參數如下:

  • item: 目前元素
  • index: 目前元素的 index
  • array: array 本身

如果我們希望按照順序印出所有商品資訊,可以這樣寫:

data.forEach(function(item) {
  console.log(item.name + '的價錢是' + item.price);
});

// 蘋果的價錢是100
// 香蕉的價錢是200
// 橘子的價錢是300

如果要知道現在遍歷到第幾個元素,可以用 index

data.forEach(function(item, index) {
  console.log(`第 ${index} 個元素是 ${JSON.stringify(item)}`)
});

// 第 0 個元素是 {"name":"蘋果","price":100}
// 第 1 個元素是 {"name":"香蕉","price":200}
// 第 2 個元素是 {"name":"橘子","price":300}

map()

當我們需要將 array 中所有元素轉換成另一種形式的資料的時候,可以使用 map()

map 接受一個 callback 函式作為參數,callback 需回傳轉換後的元素:

let result = array.map(function(item, index, array) {
  // Do something...
  return result;
})

map 會回傳一個長度一樣的 array,裡面有轉換過後的資料。

如果我們想要算出商品打折完含運費的價格,可以這樣寫:

const totalPrice = data.map(function(item, index, array) {
  return item.price * 0.9 + 10;
})

console.log(totalPrice); // [100, 190, 280]

mapforEach 最大的差異在於,forEach 不會有回傳值,但 map 會回傳一個全新的 array

如果你不需要對資料作轉換,例如:印出字串等不會回傳值的情況,就用 forEach;如果你需要資料轉換之後的結果,就用 map。

filter()

filter 用來「過濾出陣列中符合條件的元素」,會回傳一個 array。

如果想找出所有價錢大於 150 元的商品,可以這樣寫:

const filtered = data.filter(function(item) {
  return item.price > 150;
});

console.log(filtered); // [ { name: '香蕉', price: 200 }, { name: '橘子', price: 300 } ]

find()

find 用來「找出陣列中第一個符合條件的元素」,回傳值是一個元素或 undefined

如果我想找出第一個價錢大於 150 元的商品,可以這樣寫:

const found = data.find(function(item) {
  return item.price > 150;
});

console.log(found) // { name: '香蕉', price: 200 }

filter 的不同之處在於,filter 會回傳包還所有符合條件的項目的「陣列」,而 find 會回傳第一個符合條件的「項目」。

every()

every 用來測試陣列中的所有項目是否符合某個條件,回傳值是布林。

測試是否所有商品的價錢都大於 150 元:

const test = data.every(function(item) {
  return item.price > 150;
});

console.log(test); // false

測試是否所有商品的價錢都大於 70 元:

const test = data.every(function(item) {
  return item.price > 70;
});

console.log(test); // true

some()

some 用來測試陣列中是否存在至少一個項目符合某個條件,回傳值是布林。

測試是否存在商品價格大於 300 元:

const test = data.some(function(item) {
  return item.price > 300;
});

console.log(test); // false

測試是否存在商品價格大於 250 元:

const test = data.some(function(item) {
  return item.price > 250;
});

console.log(test); // true

someevery 的差別是,every 必須「所有」的項目都符合條件才會回傳 truesome 只要其中一個項目符合條件就會回傳 true

reduce()

reduce 的功能是將陣列中的所有項目合併成一個值。

reduce 的 callback 參數接受兩個參數,分別為:

  • accumulator:目前累積的值
  • item:目前項目

舉個例子,如果我們要知道陣列中所有項目加起來的總和,可以這樣寫:

const arr = [1, 2, 3, 4];
const sum = arr.reduce(function(accumulator, item) {
  console.log('accumulator: ' + accumulator);
  console.log('item: ' + item);
  return accumulator + item;
});

// accumulator: 1
// item: 2

// accumulator: 3
// item: 3

// accumulator: 6
// item: 4

console.log(sum); // 10

reduce 預設情況下從第二個元素開始遍歷,此時 item 等於 2,accumulator 預設是第一個元素 1,回傳值是 1 + 2 = 3。

接著遍歷至第三個元素,此時 item 等於 3,accumulator 是前一步的結果 3,回傳值是 3 + 3 = 6。

接著遍歷最後一個元素 item 等於 4,accumulator 是前一步的結果 6,回傳值是 6 + 4 = 10。

initialValue

reduce 可以選擇性地給第二個參數 initialValue

這個參數的使用時機是資料加總的起始值不為 0,或是資料並非數值格式。

例如,我想要知道所有的商品加起來的總價格:

const sum = data.reduce(function(accumulator, item) {
  console.log('accumulator: ' + accumulator);
  console.log('item: ' + JSON.stringify(item));
  return accumulator + item.price;
}, 0);

// accumulator: 0
// item: {"name":"蘋果","price":100}

// accumulator: 100
// item: {"name":"香蕉","price":200}

// accumulator: 300
// item: {"name":"橘子","price":300}

console.log(sum); // 600

注意 reduce 的第二個參數 0。若是提供了 initialValue 參數,便會從第一個元素開始遍歷,並且用 initialValue 當作 accumulator 的值。

第一步遍歷第一個元素,此時 accumulator 等於 0,item 是第一個元素,回傳值是 0 + 100 = 100。

第二步遍歷第二個元素,此時 accumulator 等於 100,item 是第二個元素,回傳值是 100 + 200 = 300。

第三步遍歷第三個元素,此時 accumulator 等於 300,item 是第三個元素,回傳值是 300 + 300 = 600。

結論

本篇介紹了使用 functional programming 的好處,寫程式的重點從「做什麼」轉移到「如何做」,讓你能夠專注在真正重要的邏輯上。

接著介紹 JavaScript 內建的 7 種常用陣列方法:

  • forEach()
  • map()
  • filter()
  • find()
  • every()
  • some()
  • reduce()

能夠讓你用 functional programming 的方式完成許多複雜的工作。

最後希望大家可以試著在日常工作使用看看本篇介紹的方法,相信一定能感受其簡潔與優雅!

有任何問題或想法,歡迎在下方留言給我,也可以追蹤我的 Facebook 粉絲團,才不會錯過最新文章的通知喔!

FB 粉絲團開張囉!

👍按讚才不會錯過最新的優質技術文章!👍