天天看點

資料結構之-數組

數組無處不在,當你需要多于一個資料的傳輸或者展示的時候,就會用到數組。

現在我們從定義說起。

數組定義

數組就是一組資料,表現形如:

[1,3,5,7]
複制代碼
           

JavaScript中,數組元素可以是任意類型,不過通常,同一類型的一組資料更常見,也更有實際意義。

可以用下面幾種方式定義數組:

  • 字面量
  • Array()構造函數

字面量

最簡單直覺,也是最常用之一。

let arr = [1,2,3]
複制代碼
           

這裡有兩個需要注意的現象:

let arr = [1,,3]  //通路arr[1]會是undefined
let arr = [,,] //數組長度會是2
複制代碼
           

Array()構造函數

構造函數建立數組是最合理的方式之一。就像下面這樣。

let arr1 = new Array(1,2,3,4)   //[1, 2, 3, 4]
let arr2 = new Array(1)  // [ <1 empty item> ]
let arr3 = new Array('1')  // [ '1' ]
複制代碼
           

注意:

  • 當構造函數的參數是多項時,會将每一項作為元素建立出來。
  • 當僅有一個數字時,表示數組的長度,元素并未填充,通路也會是undefined。
  • 僅有一個非數字,則又會建立一個單個元素的數組。

常見數組方法及應用

數組的方法多且用途廣,它們是數組強大的關鍵。

如果你是初學者,不要指望靠看就能記住,想當年,筆者把《高程》3 的數組章節看了幾遍都沒記住,不過,并不代表沒有幫助記憶的方法,将其和實際應用場景相結合,就可更好地記憶。

我們逐一清點。

類型判斷——Array.isArray()

有些時候,我們要判斷拿到的資料是不是數組,否則可能出現方法調用報錯的情況,通常會選用以下幾種方式:

  • instanceof

判斷運算符的左側是否是右側類型的一個執行個體,即 a instanceof b。此時我們将b設為Array即可,傳回布爾值。

  • constructor

前面我們聊建立對象的時候,提到過constructor,引用類型都有對應的constructor,數組執行個體的constructor是Array,是以,可用a.constructor == Array是否為true來檢查a是否為數組執行個體。

  • Object.prototype.toString.call()

這個方法,旨在将執行個體的類型轉為字元串然後輸出,如果執行個體是數組類型,則會輸出'[object Array]',進而可以做出判斷。因為前兩個方法可能存在一些不确定的問題,這個方法,曾被認為是最準确和可靠的方法,判斷其他引用類型也同樣。

ES6的出現提供了新的判斷方法——isArray(),隻需要Array.isArray(obj)即可判斷obj是否為數組,提供了極大的便利。

添加及删除——push()/pop()、unshift()/shift()

數組被建立後,可能有元素,也可能沒有元素,這兩組方法,常被用來動态地向數組中添加或者删除元素,隻是它們的方式有所局限,隻能從數組的兩端進行操作,但對于适合的場景來說夠用了。

什麼是适合的場景?隻要求符合條件的元素,不講究順序,也沒有其他附加條件,就可以這樣簡單粗暴地處理。

const arr = [],ele;
if(ele > 1){
    arr.push(ele)  //對應的删除即pop()
}
複制代碼
           

另外一對同理。

任意位置添加或删除——splice()/slice()

既然上面的方法有局限,這組就更靈活,它的靈活展現在不再局限位置,可在任意位置進行添加、删除、替換,至于是哪一種,取決于傳參的情況。

參數格式:splice(index,nums,item1,.....,itemX)

它們分别代表:

index——開始位置

nums——空出位置數

item1,.....,itemX——從空出的位置添加進哪些元素

前兩個參數必填,第三個選填。由此可得出:

隻要給index賦一個合法的值,就可以標明操作位置,第二位如果是0,則不删除元素,此時若第三個參數有值,則往指定位置添加元素。

如果第二個參數是非0的正整數,則删除指定數量的元素,此時第三個參數如有資料,則填到删除了元素的位置,起到元素替換的效果。

那麼slice()又是什麼,它有何不同?

slice看起來跟splice很像,隻差一個字母,但用途大不同,主要兩點差別:

一、slice接收兩個參數,begin 和 end,決定了截取源數組的哪些部分,截取出的部分包括begin,但不包括end

二、slice傳回一個新數組,這個新數組是源數組的一個淺拷貝,源數組不受影響

是以,這兩種方法的使用可簡單概括為:如果想要在源數組的基礎上做處理,截取某部分,但不改變源數組,用slice,其他情況用splice。

特定元素的索引——indexOf()/findIndex()

前面的方法中,我們提到了“元素”和‘位置’,很多時候并不知道某元素所在的位置,要動态擷取,這時候查找索引就派上了用場。

這兩種方法所得結果類似,但用法存在差異。

  • indexOf(searchElement[, fromIndex])

indexOf()方法需要傳入具體的查找元素,和起始索引(可選)。

const nums = [1, 2, 3, 4, 5];

nums.indexOf(3); //2
複制代碼
           
  • findIndex()方法則是傳入一個回調函數

函數支援三個可選參數:元素、索引、數組本身。通常,使用前兩個,甚至一個參數就夠了。像下面這樣:

const nums = [1, 2, 3, 4, 5];
let targetIndex = nums.findIndex(target => target  == 3) //2
複制代碼
           

要特别注意的是,有時候結果可能跟期望不同,即當數組中有多個相同目标元素的時候,它們都隻會傳回第一個目标元素的位置。

const nums = [1, 2, 3, 3, 5];
nums.indexOf(3); //2
let targetIndex = nums.findIndex(target => target  == 3) //2
複制代碼
           

這是正常情況,如果異常,比如元素不存在,二者均會傳回-1。

查找元素——includesOf()/find()

上一組方法,是找到某元素在數組中的位置,當然,順便可以通過傳回值是不是-1來判斷元素是否存在,而這一組方法,則是直接得到元素是否存在于數組中。

  • includesOf()——傳回布爾值
const nums = [1, 2, 3, 3, 5];
nums.includes(1) //true
nums.includes(8) //false
複制代碼
           
  • find()-傳回目标值本身
const nums = [1, 2, 3, 3, 5];
nums.includes(1) //1
nums.includes(8) //undefined
複制代碼
           

填充——fill()

上面講建立數組的時候,說可以建立一個空數組,然後往裡添加,也可使用字面量建立現成的數組,也可使用splice對數組進行增、删、改,但還有一種方式可以用來改變數組——fill()。

看看用法。

const arr = new Array()
arr.fill(1) //[]
複制代碼
           

哦豁~好像翻車了,說好的填充呢,怎麼還是空數組?

且慢,fill()方法不是這麼用滴,使用它的前提是,數組中已有一定數量的元素。比如:

可以這樣:

const arr = new Array(1,2,3,4)
arr.fill(5) // [ 5, 5, 5, 5 ]
複制代碼
           

也可以這樣

const arr = new Array(4)
arr.fill(5) // [ 5, 5, 5, 5 ]
複制代碼
           

由此,能夠得出一個快速建立具備某數量的非空數組的方法。

現在來看看完整文法:arr.fill(value[, start[, end]])

似曾相識吧,它也接收兩個位置參數,一個起始位置,一個結束位置,上面我們沒有傳的時候,它們預設是從頭到尾,我們可以設定試試看。

const arr = [1,2,3,4,5,6]
arr.fill(5,2,4) // [ 1, 2, 5, 5, 5, 6 ]
複制代碼
           

但是,fill()方法有個易犯的錯誤,當填充的元素是引用類型時,其填充的值都會是同一個引用,比如,初始化一個商品清單。

const goods = {
    name:'珠寶',
    price:10,
    weight:'20'
}

const goodsList = new Array(8)
goodsList.fill(goods)
複制代碼
           

此時的商品清單資料會是這樣:

[
  { name: '珠寶', price: 10, weight: '20' },
  { name: '珠寶', price: 10, weight: '20' },
  { name: '珠寶', price: 10, weight: '20' },
  { name: '珠寶', price: 10, weight: '20' },
  { name: '珠寶', price: 10, weight: '20' },
  { name: '珠寶', price: 10, weight: '20' },
  { name: '珠寶', price: 10, weight: '20' },
  { name: '珠寶', price: 10, weight: '20' }
]
複制代碼
           

然後我們編輯第一個商品,将價格改為8

goodsList[0].price = 8
複制代碼
           

卻發現每個商品的價格都被改變了。

[
  { name: '珠寶', price: 8, weight: '20' },
  { name: '珠寶', price: 8, weight: '20' },
  { name: '珠寶', price: 8, weight: '20' },
  { name: '珠寶', price: 8, weight: '20' },
  { name: '珠寶', price: 8, weight: '20' },
  { name: '珠寶', price: 8, weight: '20' },
  { name: '珠寶', price: 8, weight: '20' },
  { name: '珠寶', price: 8, weight: '20' }
]
複制代碼
           

這顯然不是想要的效果。

不僅如此,别忘了數組也是引用類型,是以,在初始化二維數組時同樣會有這個問題,是以,如果有這樣一個需求——在頁面初始化時,需要準備好一組待編輯/修改的資料項,就不适合用這種方法來建立了。

排序——sort()

排序是個常見需求,凡涉及清單,定有排序,按時間、按價格、按銷量等。

最簡單的,給一組數字排序。

const numSort = [3,2,8,6,5,7,9,1,4].sort()
numSort //[1, 2, 3, 4, 5, 6, 7, 8, 9]
複制代碼
           

這麼簡單,有什麼可說?

當然有,一個小例子就成功欺騙了我們,它是按照數字大小從小到大排列?非也,不信再看。

我們将上面的數組改一下:

const numSort = [3,2,8,10,6,20,5,7,9,1,4].sort()
//[1, 10, 2, 20, 3, 4, 5, 6, 7, 8, 9]
複制代碼
           

神奇的事情發生了,10比2小?20比3小?

注意了,sort()方法實際接收一個函數,以函數的傳回值來指定按某種順序排列,如果省略函數,則按照将元素轉為字元串的各字元的Unicode位點進行排序。是以,如果這裡想要按照數字的真實大小排序,可以這樣寫。

[3,2,8,10,6,20,5,7,9,1,4].sort((a,b) => a-b)
複制代碼
           

依據是什麼?是排序函數的算法規則:

  • 如果 a - b 小于 0 ,a 會被排列到 b 之前;
  • 如果 a - b 等于 0 ,a 和 b 的相對位置不變;
  • 如果 a - b 大于 0 ,b 會被排列到 a 之前。

如果比較對象是字元串,方法也一樣,是以,一般情況下,不要偷懶,我們可以充分運用這個特點,對需要的規則進行定制。

上面讨論的是對數字或者字元串進行排序,日常需求中,往往不會這麼簡單,可能會對一列包含多個屬性的對象數組進行排序,比如開始提到的:價格、銷量等。

怎樣根據某個屬性對數組排序。

其實也不難,同樣道理,拿上面的商品清單(goodsList)為例,如果以價格排序,隻需要這樣:

goodList.sort((a,b)=>{
    return (a.price - b.price)
})
複制代碼
           

就可以了。

說了排序,順便說下反轉(reverse),反轉也是一種排序,隻是它沒什麼規則可言,直接将一組元素首尾颠倒,[1,2,3]會變成[3,2,1],['a','f','c']變成[ 'c', 'f', 'a' ]。

合并——concat()/擴充運算

理想情況下,我們擷取一個數組,操作一個數組是最好,但有時資料來源不止一個,可能是兩個或多個,在展示或傳遞的時候,又需要合為一個,就要用合并方法,傳統方法是concat()。

const primary = [1,2,3,4,5,6], 
      middle = [7,8,9], 
      high = [10,11,12];
const grade = primary.concat(middle,high)  
//[1,  2, 3, 4,  5,  6,  7, 8, 9, 10, 11, 12]
複制代碼
           

但是,如果覺得僅此而已,就又錯了。

  • concat()不僅可以用來合并數組,還可以合并一個數組和其他類型的值,比如數字、字元串等。
primary.concat(7,8) // [1,  2, 3, 4,  5,  6, 7, 8]
複制代碼
           
  • concat()在合并數組時,不改變原數組,而是傳回新數組,但是,新數組包含的是原數組的淺拷貝。
const primary = [{a:1}], 
      middle = [{b:2}];
const grade = primary.concat(middle)
//[ { a: 1 }, { b: 2 } ]
primary[0].a = 2
middle[0].b = 3
// [ { a: 2 }, { b: 3 } ]
複制代碼
           

引用類型總是帶給我們“驚喜”,在使用時要多加注意。

當然,擴充運算符依然是簡潔。上面的操作隻需要這樣就可以:

const grade = [...primary,...middle,...high]
複制代碼
           

傳回新數組——map()/filter()

新數組是什麼意思?

大部分情況下,我們拿到的資料都是由對象組成的數組,對象是集合,會包含很多東西,它本身的資料、它關聯的其他資料等,少則幾條,多則幾十條,但在傳遞或者展示的時候并不需要把它們都帶着,或者,我們需要在原有基礎上進行處理,這時候就可以按需傳回處理後的新數組。

比如下面這樣:

[
  { name: '珠寶', price: 1000, weight: '20' },
  { name: '手機', price: 2000, weight: '20' },
  .
  .
  .
  { name: '電腦', price: 5000, weight: '20' }
]
複制代碼
           

我們得到一個商品清單,但隻需要把name拿出來用,就可以這樣。

let nameList = goodsList.map(item=>{
    return item.name
})
['珠寶'.'手機',...,'電腦']
複制代碼
           

又或者,我們需要在原價的基礎上,對所有商品進行打折處理,就可以這樣:

let priceDiscountList = goodsList.map(item=>{
    item.price *= 0.5 
    return item
})
[
  { name: '珠寶', price: 500, weight: '20' },
  { name: '手機', price: 1000, weight: '20' },
  .
  .
  .
  { name: '電腦', price: 2500, weight: '20' }
]
複制代碼
           

當然,實際項目中這個環節不會這麼幹,不然每有變動都要改JS邏輯代碼,從易用性、效率和維護上都不利,隻是借此說明用法。

介紹完map,再看filter,從字面意思很好了解,過濾,符合條件才會被篩選出來,它同樣是接收一個函數。

比如我們将價格超過500的找出來。

let priceLowList = goodsList.filter(item=>{
    return item.price > 500
})
[
  { name: '手機', price: 1000, weight: '20' },
  .
  .
  .
  { name: '電腦', price: 2500, weight: '20' }
]
複制代碼
           

這兩個方法在實際項目中極為常見,唯獨需要注意的是它們的工作方式,它們都是生成新的數組,map需要傳回的是數組元素,fliter則是篩選條件,千萬記得”return“哦!

疊代處理——forEach()

這個方法,和上面兩個極為相似,從底子上,都是可以通路到數組的每個元素,然後進行相應處理,差別在于,此方法僅用于疊代,好比以前常用的for循環,當然,功能的簡單意味着可操作空間更大。

比如,我們可以這樣實作類似map的效果。

let nameList = []
goodsList.forEach(item=>{
    nameList.push(item.name)
})
複制代碼
           

也可以這樣實作類似filter的效果。

let priceLowList = []
goodsList.forEach(item=>{
    if(item.price > 500){
        priceLowList.push(item)
    }
})
複制代碼
           

是的,你可以寫任何想要的邏輯,且它的執行效率比for循環更高,也更符合函數式程式設計範式。

元素判斷——some()/every()

同樣用于檢查,接收回調函數,寫入判斷邏輯,差別在于,some()是“存在符合”即為true,而every()是“所有符合”才為true,類似 || 和 &&。比較簡單,不再贅述。

去重

當使用者的操作是大量的、不确定的,難免有重複,有時候我們隻需要知道某個值是否存在,而不是多個重複的值,這時就需要對數組進行去重(當然,還有其他方法保證單一值,這裡重點是去重)。

去重方法有很多,原理是類似的——通過周遊數組做比較,保證值唯一。列三種大家參考:

  • includes
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array =[];
    for(var i = 0; i < arr.length; i++) {
        if( !array.includes( arr[i]) ) {   //檢測數組是否有某個值
            array.push(arr[i]);
        }
    }
    return array
}
複制代碼
           
  • filter
function unique(arr) {
    return arr.filter(function(item, index, arr) {
      //目前元素,在原始數組中的第一個索引==目前索引值,否則傳回目前元素
      return arr.indexOf(item, 0) === index;
    });
  }
複制代碼
           
  • Set
function unique (arr) {
  return Array.from(new Set(arr))
}
           

繼續閱讀