JS 陣列方法 map()
一起來畫地圖
最後更新時間:2023–05–20
聽說有個說法叫陣列三寶?
曾在某個地方聽人說 JS 陣列三寶是 map()、filter()、reduce(),當時覺得挺有趣的便記了下來,reduce() 我曾在我是數學小老師這篇提到,而這幾天剛好用到 map() 想說就來寫一篇 map() 吧,在寫這篇時特地去查陣列三寶,但用關鍵字卻 google 不到什麼資料,後來索性去問 ChatGPT:
「請問 JavsScript 裡面有所謂的陣列三寶嗎?」
結果它回答:
是的,在 JavaScript 中有一些常用的陣列方法被稱為「陣列三寶」,它們分別是 map()、filter() 和 reduce()。
嚇歪,竟然跟聽到的一樣,看來不是空穴來風,於是我接著問:
「請問陣列三寶這個說法是哪裡來的?」
它回答:
「陣列三寶」是一個俚語術語,常用於描述 JavaScript 中最常用的三個陣列方法。這個術語的起源並不是很明確,但據說最早是在一些 JavaScript 社群中被使用的,隨後逐漸流傳開來。這個術語的含義是指這三個陣列方法在 JavaScript 中非常常用,且在很多場合下都能夠發揮重要的作用,因此被形象地比喻為「三寶」。這個術語的使用在 JavaScript 開發者社群中非常普遍,並且已經成為了一種口語化的簡寫方式。不過需要注意的是,雖然這三個方法非常常用,但並不是所有的 JavaScript 開發者都會使用這個術語,而且也有其他常用的陣列方法,例如 forEach() 和 some() 等。
看來是我在社群混跡短短幾個月太淺了,沒聽其他人講過是自己道行不夠,但沒關係,今天就先來複習一下 map() 吧。
說文解字時間到(以下都是個人理解,本人不是語言學家)
在複習 map() 之前先來複習 map,當名詞當然就是地圖,如果是動詞那可以是繪製 xxx 的地圖、在地圖上標示出,而有個名詞叫 mapping,意思是匹配、映射,其實都可以感覺得到 map 這個字大概跟「依據 xxx 去關聯到 yyy」有關,所以用這個字來當作方法名稱還算恰當。
map() 方法簡單說就是將某陣列裡的元素依照你想要的方式處理過後(不影響原元素),放進一個新陣列給你(跟原陣列不同,是個新陣列),所以就很像剛剛說的「依據原陣列元素以你想要的方式去關聯到新陣列」,那就來看看怎麼用吧。
map() 用法
最常見的莫過於處理數字了吧,例如把原陣列數字都乘以 2:
let numA = [1, 2, 3]
let numB = numA.map( function(e) {
return e*2
})
console.log(numB) // 印出 [ 2, 4, 6 ]
而 map() 裡的函式參數可以用箭頭函式簡化:
let numA = [1, 2, 3]
let numB = numA.map( e => e*2)
console.log(numB) // 印出 [ 2, 4, 6 ]
map() 也可以拿來方便處理物件陣列,例如我現在有一些水果要賣:
let fruit = [
{ item: "Apple", price: 10 },
{ item: "Banana", price: 20 },
{ item: "Cherry", price: 30 }
];
想直接獲取一個包含所有水果名稱的陣列,可以這樣寫:
let fruitNames = fruit.map(e => e.item);
console.log(fruitNames); // [ 'Apple', 'Banana', 'Cherry' ]
如果是想獲得打九折後的價格則這樣寫:
let priceNew = fruit.map(e => e.price * 0.9);
console.log(priceNew); // [ 9, 18, 27 ]
那如果想要得到跟原物件陣列一樣的內容但是價格打九折呢?可以這樣寫:
let fruit = [
{ item: "Apple", price: 10 },
{ item: "Banana", price: 20 },
{ item: "Cherry", price: 30 }
];
let fruitNewPrice = fruit.map(e => {
return {item: e.item, price: e.price * 0.9};
});
console.log(fruitNewPrice)
/*
印出:
[
{ item: 'Apple', price: 9 },
{ item: 'Banana', price: 18 },
{ item: 'Cherry', price: 27 }
]
*/
最後一個稍微解釋一下,我們要讓每個 e(也就是 element 元素)都加工成一個東西並放到新陣列裡面,那要回傳什麼?就是回傳一個物件,物件的 key 即為原物件的 key,所以新的 item 就維持是 e.item,而新的 price 則是原 price 再乘上 0.9。
稍微提高一些難度,就稍微
接下來說一下比上面再複雜一些些的用法
其實根據 MDN,map() 裡面放的函式可以有 3 個參數(請注意這 3 個參數是放在該函式裡而非直接放在 map() 裡),所以可以長這樣:
let newArr = arr.map(function (value, index, array){
//...
});
如果只是把 map() 當作將原陣列簡單加工處理並得到所需新陣列的工具,那通常只會用到第 1 個參數,也就是上面那些範例中的 e,那什麼情況會用到第 2 個甚至第 3 個?以下分別舉兩個例子說明。
1️⃣ 用到第 2 個參數 index
假設今天有兩個陣列,分別是水果品項與價格:
let items = ["Apple", "Banana", "Cherry"];
let prices = [10, 20, 30];
而他們之間有對應關係,也就是蘋果是 10 元、香蕉 20 元,那要怎麼把這兩個陣列揉合一下產生出一個新陣列長得跟下面這個先前的例子一樣呢:
let fruit = [
{ item: "Apple", price: 10 },
{ item: "Banana", price: 20 },
{ item: "Cherry", price: 30 }
];
如果是這個情境,那就會用到函式裡的第 2 個參數 index 了,因為我們已知他們之間存在的關聯性就是順序相同,所以可以靠 index 讓兩者依照這個關聯去加工:
let items = ["Apple", "Banana", "Cherry"];
let prices = [10, 20, 30];
let fruit = items.map((e, index) => ({ item: e, price: prices[index] }));
console.log(fruit)
/*
印出:
[
{ item: 'Apple', price: 10 },
{ item: 'Banana', price: 20 },
{ item: 'Cherry', price: 30 }
]
*/
這邊解釋一下,我想要的東西就是 fruit 這個物件陣列,那這裡面應該要有 3 個物件,各自是一組水果與價格,那我要怎麼獲得這些物件?即針對 items(水果品項)去用 map() 加工,以下分成水果與價格兩個部分來看:
- 水果:令新物件 item(即物件第 1 個 key)的值(即物件 key 對應的 value)為原 items 陣列裡的水果名稱,這樣就完成了新物件中水果的部分
- 價格:新物件 price(物件第 2 個 key)的值(key 對應的 value)為原 prices 陣列裡對應的價格,這邊要注意,這裡的 index 是指處理到的 e 的 index,例如 Apple 的 index 就是 0,因為已知原本兩個陣列的對應順序相同,所以假設處理到 Apple,那就會回傳 prices[0](也就是 10) 去當 price 的值,因此這邊重點是用 Apple 的 index 去抓 prices 裡面該 index 的值
最後,當 Apple、Banana、Cherry 這 3 個水果都跑過後,自然會回傳 3 個新物件,並且被包在一個新陣列裡。
第 1 個例子講完了,但這邊插播問個小問題,在上述這段程式碼中:
let fruit = items.map((e, index) => ({ item: e, price: prices[index] }));
為什麼箭頭右邊不是直接回傳物件就好,還要用一組括號包起來?
嘿嘿,可以想想看
想到了嗎?
可以反向思考,如果沒有那組括號包起來會怎樣?
如果有去試試會發現,這樣寫會噴錯,為什麼?
要講囉,原因是,如果沒有那組括號電腦會以為你要用一個 function 去做事,因為箭頭函式在箭頭右邊本來就是接大括號然後裡面寫你要做的事。箭頭函式未簡化前長這樣:
let fruit = items.map( function(e, index) {
return { item: e, price: prices[index] }
});
console.log(fruit)
/*
印出:
[
{ item: 'Apple', price: 10 },
{ item: 'Banana', price: 20 },
{ item: 'Cherry', price: 30 }
]
*/
稍微簡化後長這樣,可以看到印出來的東西沒變:
let fruit = items.map( (e, index) => {
return { item: e, price: prices[index] }
});
console.log(fruit)
/*
印出:
[
{ item: 'Apple', price: 10 },
{ item: 'Banana', price: 20 },
{ item: 'Cherry', price: 30 }
]
*/
而如果回傳的東西很單純,是 1 行程式碼可以解決的,那還可以省略箭頭右邊的大括號跟 return 這個字沒錯吧?那就會變成:
let fruit = items.map( (e, index) => { item: e, price: prices[index] });
console.log(fruit)
// 噴錯噴爆
但這樣省略後,剛好我們要的東西是物件,物件外面是大括號包住,而箭頭函式右邊也剛好是接大括號,所以就像剛剛說的,電腦會以為你要去執行某個 function 而非回傳物件,但裡面的寫法不是一個 function 會出現的寫法,所以電腦就會告訴你它看不懂了。
沒想到看個例子也能獲得額外小知識吧,還記得現在正題講到哪嗎?我們剛講完 map() 括號裡的函式的第 2 個參數 index,讓我們回到正題並繼續講第 3 個參數 array。
2️⃣ 用到第 3 個參數 array
第 3 個好像目前真的比較少遇到,還是舉個例子說明一下。假設我有兩串數字:
let numbers = [1, 2, 3, 4];
let numbersNew = [3, 5, 7, 4]
第二串跟第一串的關聯性是,我想把 numbers 中每個數字跟其後面的數字加起來後回傳新數字,例如 1 後面有 2,那就是將 1 + 2,回傳 3;而 2 後面有 3 所以 2 + 3 回傳 5,以此類推,而最後一個數字因其後沒數字那就回傳其本身即可。如果要用 map() 實作,該怎麼寫呢?可以先想想看再往下看怎麼寫。
想到了嗎?以下是可能的寫法:
let numbers = [1, 2, 3, 4];
let numbersNew = numbers.map(function(value, index, array) {
let numberNew = value;
if (index < array.length - 1) {
numberNew += array[index + 1];
}
return numberNew;
});
console.log(numbersNew); // [3, 5, 7, 4]
如果看不是很懂,沒關係,再看一次註解版:
let numbers = [1, 2, 3, 4];
let numbersNew = numbers.map(function(value, index, array) {
let numberNew = value; // 請注意這裡是單數 numberNew
if (index < array.length - 1) { // 若 index 小於 3 代表不是最後一個數字
numberNew += array[index + 1]; // 那就將原數字跟它後面的數字相加
}
return numberNew; // 回傳相加後的新數字
});
console.log(numbersNew); // [3, 5, 7, 4]
可以看得出來,會用到第 3 個參數 array 的情況有可能是需要運用到整個陣列的一些特性(例如 length)去做一些操作。
注意事項
使用 map() 有一些事情可以留意:
- 會回傳一個新陣列
- 基本上不會改到原陣列,如果你想要也可以改,但除非有特殊目的不然一般不會這麼做
- map() 裡的函式中可以放 3 個參數,而這邊指的函式是 map() 括號中的第 1 個參數,還可以有第 2 個參數叫 thisArg,但在此不贅述
問題?
我曾在 slice() 與 splice() 聰明分得清楚 文中闡述我對於一次學好幾個很像的東西並且彼此對照的想法,有興趣可以看看,簡單說其實有更好的做法,例如先針對其中一個刻意練習,練熟了再去做其他的就比較不會搞混,而不是一開始就把很像的東西列在一起學。為什麼要談這件事?因為學習 map() 也會有這個問題,例如新手可能也用過 forEach(),那 map() 跟 forEach() 差在哪?那再加入一個 filter() 一起比較呢?嘿嘿,偏不在這邊一起談,以後有機會再說,先把 map() 練熟吧~