iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 12
1
Modern Web

JavaScript基本功修煉系列 第 12

JavaScript基本功修練:Day12 - 陣列常用方法:indexOf、splice、concat與展開運算子

  • 分享至 

  • xImage
  •  

陣列的方法真的很多,多到可以自成一個鐵人賽系列(咦找一下還真的有,太厲害了!!) 但這裏因為內容有限,所以只能重點講比較常用的方法,以及一些應用例子。

今天會針對整理關於indexOfspliceconcat與展開運算子的知識。

indexOf

indexOf的用法很直接,就是尋找在陣列中的某個值。

array.indexOf(想尋找的元素,索引值起點)
  • 如果在陣列中找不到想尋找的值,就會回傳-1
  • 如果你想尋找的值有重複出現,indexOf就只會回傳最先出現的那個值
  • 尋找值時一律是由左至右開始找,並用===全等的比較方式去尋找你指定想尋找的值
  • 如果索引值起點是負整數,就會從陣列中最後一個值往回數,最後一個值的索引是-1,倒數第2個就是-2,如此類推,之後再從左至右開始尋找。

splice

splice可以刪除陣列中的值。它的回傳值是被刪除的值:

array.splice(要開始改動的索引值,刪除多少個元素,想加入的元素)

新手的我平時很少用第3個参數,看看MDN才知道原來也可以這樣去用,例子如下:

const cartoon = ['pikachu','rickAndMorty','simpsons']

//不刪除值,只把'onePiece'加到索引值是1的位置
cartoon.splice(1,0,'onePiece')
console.log(cartoon) //["pikachu", "onePiece", "rickAndMorty", "simpsons"]

//刪除值,以及換上另一個值
cartoon.splice(2,1,'naruto')
console.log(cartoon) //["pikachu", "rickAndMorty", "naruto"]

應用例子(indexOf、splice):

Q1) 我想刪除一筆資料,但不確定該陣列有沒有該筆資料

const city = ['Hong Kong', 'Tokyo', 'Seoul', 'Taipei', 'BangKok', 'Singapore']

const deleteCity = city.indexOf('BangKok');


if(deleteCity > -1){
    city.splice(deleteCity,1)
}
console.log(city) //["Hong Kong", "Tokyo", "Seoul", "Taipei", "Singapore"]

以上例子,利用不存在陣列裏的index會等於-1的特性,判斷該陣列裏是否有想刪除的資料,如有,就用splice語法把它刪除。

當然方法也有多種,例如我們也可以用filter

const newCity = city.filter( (cityName) => {
    return cityName !== 'BangKok'
})

console.log(newCity) //["Hong Kong", "Tokyo", "Seoul", "Taipei", "Singapore"]

這樣就能避免修改到原陣列了。

Q2) 我想刪除重覆的資料

const city = ['Taipei', 'Hong Kong', 'Tokyo', 'Seoul', 'Seoul', 'Taipei', 'BangKok', 'Singapore']

const uniqueCity = city.filter( (cityName,index) => {
    return city.indexOf(cityName) === index  
})

console.log(uniqueCity) //["Taipei", "Hong Kong", "Tokyo", "Seoul", "BangKok", "Singapore"]

這裏用到indexOf是找出該資料在陣列裏,第一次出現的索引值,而()裏index是指正在處理中的該筆資料,在陣列中的索引值

例如'Taipei'這個字串,在陣列中第一次出現的索引值是0,但當這程式跑到那個重複的'Taipei'時,它此時在陣列的索引值是5,並不等於第一次出現的索引值,因此可見它是重複的資料,可以被刪除掉。

題外話:這題用forEachsplice寫好嗎?

在寫這篇文章的同時,我也考慮過能不能用forEachsplice的寫法:

let city = ['Taipei', 'Hong Kong', 'Tokyo', 'Seoul', 'Seoul', 'Taipei', 'BangKok', 'Singapore']

city.forEach( (cityName, index) => {
    console.log(cityName, index, city.indexOf(cityName))
  
    if(index !== city.indexOf(cityName)){
        city.splice(index,1)
    }
})

console.log(city) //["Taipei", "Hong Kong", "Tokyo", "Seoul", "Taipei", "BangKok", "Singapore"]

結果只是有成功刪掉重覆的'Seoul',但'Taipei'沒有。之後再查一下console:

發現它竟然沒有跑到第二個'Taipei'!它是跑完第二個'Seoul'之後,就跳過第二個'Taipei',直接去跑'BangKok'。可想而知為什麼第二個'Taipei'沒有被刪掉,因為它已經被忽略掉了。

仔細想想,這是因為第二個'Taipei'前面的那個'Seoul'被刪除了,所以所有元素的索引值都變了。當前面的那個'Seoul'被刪除後,緊接住的第二個'Taipei'的索引值變成了4,但因為對forEach迴圈來說,它已經跑過4了,下一次會跑5,而這時候的5已經不是'Taipei',而是'BangKok'。所以第二個'Taipei'就直接被跳過了。

總括而言,這個方法就是很容易會埋下一些問題,因為我在跑forEach同時又修改了forEach正在迭代的資料,所以並不建議這樣做~(但好像只有我在想這些蠢問題XD

concat

concat會合併陣列或值。使用時要注意:它會淺拷貝原本的陣列,所以當原陣列被修改後,它的拷貝都會被修改。

const a = [2,[3],4]
const b = [5,6,[7]]
const sum = a.concat(b)

console.log(sum) //[2,[3],4,5,6,[7]]

//a被修改
a[1].push('x','y')

//sum也會被修改
console.log(sum)//[2,[3,"x","y"],4,5,6,[7]]

展開運算子

在閱讀有關concat的文章時,不少文章都會一併談及展開運算子(Spread syntax(...)),因為展開運算子與concat一樣能合併陣列,而且使程式碼更易閱讀。

展開運算子可以用來:

  • 合併陣列 (作用與concat一樣)
  • 當作参數傳入函式(ES6前會用apply)
  • 把類陣列轉成陣列
  • 展開可迭代的元素(物件、字串)

展開運算子可以展開可迭代的元素,除了陣列,還包括物件字串。但這裏只會針對陣列的情況作解釋為主。

合併陣列

除了concat,我們也可以用展開運算子來合併陣列,而且跟concat一樣,展開運算子是屬於淺拷貝的做法。

寫法範例如下:

/*--- 合併陣列 ---*/
const arrayA = ['JavaScript','HTML','CSS']
const arrayB = [100,200,300]
const arrayC = [[2],[4]]

const combinedArray1 = [...arrayA,...arrayB,...arrayC]
const combinedArray2 = arrayA.concat(arrayB,arrayC)

//上面兩者都會回傳 ["JavaScript", "HTML", "CSS", 100, 200, 300, [2], [4]]


/*--- 合併陣列(在中間插入) ---*/
const arrayD = ['Vue','React',...arrayB,'Angular']
console.log(arrayD) 
//["Vue", "React", 100, 200, 300, "Angular"]

從例子可見,用展開運算子的寫法比concat的寫法更易閱讀!

合併物件也可以,但不同於陣列,如果有屬值重複,就會被蓋掉

//合併物件
const foodA = {name: 'apple', color: 'red'};
const foodB = {name: 'banana', color: 'yellow', price: 14};
const foodMerge = {...foodA,...foodB}

//注意,如果屬性重複,就會被最後一個物件的屬性覆蓋掉
console.log(foodMerge) //{name: "banana", color: "yellow", price: 14}

展開陣列,把值當作参數傳入函式

const num = [100,200]

function sum(a,b){
    console.log( a + b )
}

//以下兩者回傳結果都是一樣
sum(...num) //300
sum.apply(null, num) //300

把類陣列轉成陣列

每個函式(除了箭頭函式)裏,都會有隱藏物件arguments,它以類陣列的形式呈現每一個被傳進函式裏的参數。以下例子可見,我們可以用展開運算子把類陣列轉為陣列:

function func(x,y){
    console.log(arguments) //回傳類陣列
    console.log(Array.isArray(arguments)) //false

    const convertToArr = [...arguments]; 
    console.log(convertToArr); //[123,'456']
    console.log(Array.isArray(convertToArr)); //true
}

func(123,'456')

展開可以被迭代的物件或字串

上文曾經提及我們還可以展開物件或字串,例如:

const word = 'hello'
const wordArr = [...'hello']
console.log(wordArr) //["h", "e", "l", "l", "o"]

當展開物件時,前題是物件能夠被迭代,物件本身是不能被迭代的,但有些情況下除外,例如物件本身是被放在陣列裏時就可以被迭代。MDN的解釋:

Objects themselves are not iterable, but they become iterable when used in an Array, or with iterating functions such as map(), reduce(), and assign(). When merging 2 objects together with the spread operator, it is assumed another iterating function is used when the merging occurs.

例如我把單純地把一個物件與陣列合併,這時候該物件是不能被展開的,因為它是不能被迭代。

const obj = {name:'Mary',age: 13,address:'abc st.'};
const objCombineArr = [...obj,12,3]
//Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator)

應用例子(concat、展開運算子)

Q1) 把二維陣列合併成一個陣列,並找出有Cabin此字的行李箱

const luggage = [
    ['Original Cabin','Classic Cabin','Original Trunk S'],
    ['Essential Cabin', 'Original Check-in M', 'Classic Check-in M'],
    ['Original Trunk S', 'Hybrid Cabin S'],
]

const allLuggage = [].concat(...luggage)
console.log(allLuggage)

const result = allLuggage.filter( (item) => {
    return item.match(/Cabin/g)   
})

console.log(result) //["Original Cabin", "Classic Cabin", "Essential Cabin", "Hybrid Cabin S"]

Q2) 自訂new Date產生的時間

let today = new Date(...[2020,5,1]) 
console.log(today) 
//Mon Jun 01 2020 00:00:00 GMT+0800 (Hong Kong Standard Time)

總結

indexOf:

  • 如果在陣列中找不到想尋找的值,就會回傳-1
  • 如果你想尋找的值有重複出現,indexOf就只會回傳最先出現的那個值

splice:

  • 除了可以刪除資料,也可以加入資料

concat:

  • 合併陣列或值
  • 屬於淺拷貝方法

展開運算子:

  • 合併陣列 (作用與concat一樣)
  • 當作参數傳入函式(ES6前會用apply)
  • 把類陣列轉成陣列
  • 展開可迭代的元素(物件、字串)

参考資料

從ES6開始的JavaScript學習生活 - 陣列
從ES6開始的JavaScript學習生活 - 展開運算符與其餘運算符
[ES6-重點紀錄] 擴展運算子 Spread Operator
ES6-展開運算子(Spread Operator)、其餘參數(Rest Operator)
展開運算符與其餘運算符
JavaScript 陣列處理:找東西 - indexOf、$.inArray 與 filter


上一篇
JavaScript基本功修練:Day11 - 陣列基本概念
下一篇
JavaScript基本功修練:Day13 - 陣列高階函數練習之Codewars刷題(I)
系列文
JavaScript基本功修煉31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言