五分鐘快速瞭解ArrayLike(類陣列)

bs123發表於2019-02-25

我在從效能優化角度探討瀏覽器重繪與重排的過程一文中提到過採用DOM提供的選擇器API,document.querySelectorAll()方法要比諸如document.getElementsByTagName()這類的方法在不同瀏覽器中的效能高出2~6倍,原因是document.querySelectorAll()方法返回的是一個NodeList(一個包含了所匹配到的節點的類陣列物件),而不是一個包含了實時文件結構的HTML集合,避免了HTML結構更新所帶來的重排或重繪操作。重繪和重排不是本次我們討論的重點,感興趣的童鞋歡迎移步:前端效能優化:細說瀏覽器渲染的重排與重繪.

上面我們提到NodeList是一個類陣列物件,那麼什麼是類陣列?類陣列和陣列又有何區別?他們之間又有什麼宿世糾葛呢,莫慌,接下來讓我們用程式碼來一探究竟。。。

什麼是類陣列

書面有這麼一句定義來闡述類陣列物件:只包含使用從零開始,且自然遞增的整數做鍵名,並且定義了length表示元素個數的物件,我們就認為它是類陣列物件。

舉個例子:

var array = [`zhangsan`, `lisi`, `zhaoliu`];

var arrayLike = {
    0: `zhangsan`,
    1: `lisi`,
    2: `zhaoliu`,
    length: 3
}
複製程式碼

程式碼塊中的arrayLike物件就是一個類陣列物件(包含了0、1、2三個索引和一個length屬性)。

但這似乎並不準確,同樣上面的程式碼,array變數是一個真正的陣列,他也包含了若干索引和length屬性啊,為什麼它就是陣列而不是類陣列呢?為了徹底搞清楚這個問題,我們從資料的讀取、長度的獲取和它們各自的遍歷三個方面來對比

類陣列和陣列的資料讀取

還是以上面的程式碼塊為例,我們分別對它們的一個值進行讀取:

var arrayVal = array[0]); // name
var arrayLikeVal = arrayLike[0]; // name

array[0] = `new name`;
arrayLike[0] = `new name`;
複製程式碼

程式碼很簡單,不必做過多解釋,但核心點可以得出一個結論:從資料獲取和值設定的角度,無論是獲取資料還是對物件屬性值進行設定,用法是非常相似的。

類陣列和陣列長度的獲取和自身遍歷

依舊是從程式碼角度對比:

// 長度獲取
console.log(array.length); // 3
console.log(arrayLike.length); // 3

// 遍歷
for(var i = 0, len = array.length; i < len; i++) {
  //  ...
}
for(var i = 0, len = arrayLike.length; i < len; i++) {
    // ...
}
複製程式碼

結果依然可以看出,類陣列和陣列的長度length值獲取和自身的遍歷運用是非常相似的。

之所以說是非常相似的,而不是相同的,是因為類陣列和陣列期間確實存在很多的不一樣,所以,對比了他們的相同的用法,接下來,我們來看看陣列和類陣列的不同點。

需要注意的是,在遇到遍歷的需求的時候,實際上陣列的效率要比類陣列要高很多,因此如果在遇到有遍歷類陣列的需求的時候(比如遍歷一個NodeList集合中的所有元素),建議先將類陣列轉化成陣列,在執行遍歷以此優化效能。

方法呼叫

我們都知道,陣列中給出了很多已有的方法方便我們對陣列進行增、刪、改、查等操作,以常用的陣列追加方法Array.push()為例:

array.push(`tianqi`) // array =  [`zhangsan`, `lisi`, `zhaoliu`, `tianqi`]

arrayLike.push(`tianqi`) // arrayLike.push is not a function
複製程式碼

也就是說,類陣列不存在push這個方法,事實上,除了上文中所說的幾個相同地方,陣列中包含的用以運算元組的方法類陣列都不存在,所以說,類陣列畢竟是類陣列,它終究不是陣列啊。

但是如果我非想要在類陣列中使用陣列中的方法,該如何處理呢?

用call/apply方法進行呼叫

我們知道,call和apply方法可以改變this指向,根據這一特性,我們可以把類陣列this指向真正的陣列上去,這樣不就可以在類陣列上使用陣列的方法了麼。以Function.call方法為例:

var arrayLike = {
    0: `name`,
    1: `age`,
    2: `sex`,
    length: 3
}

Array.prototype.join.call(arrayLike, `:`); // name:age:sex

Array.prototype.map.call(arrayLike, function(item){
    return  `${item}-map`;
});
// ["name-map", "age-map", "sex-map"]

複製程式碼

這樣,我們通過改變類陣列的this指向,間接地使用了陣列的方法。

類陣列轉換成陣列

第二種方法讓類陣列使用陣列的方法是先將類陣列轉化成真正的陣列,然後就可以順理成章的使用陣列方法了,不過但這實際上是歸納在類陣列轉換成陣列的這一點上了,和類陣列使用陣列方法並沒什麼聯絡。

根據部分陣列的方法呼叫後悔返回一個新的陣列這一特性,總結了幾種可以將類陣列轉換成陣列的方法:

// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"] 

// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"] 

// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"] 

// 4. concat
Array.prototype.concat.apply([], arrayLike)

// 5 ES6 ...運算子 作為函式引數的時候可以吧arguments轉換成陣列
function translateArray(...arguments) {
    // ...
}
複製程式碼

以上五中方法都是類陣列轉換成陣列的方法,除去es6提供的兩種方式,其他三種依據的是這些函式本身會返回一個新的陣列原理。

實際上根據這一特性,我麼還可以用它來實現陣列的快速拷貝的效果,用法和類陣列轉換陣列用法一樣。

最後總結

這一節,我麼從程式碼的角度對比類陣列與陣列的用法異同,並總結了幾種類陣列轉換成陣列的方式以及類陣列中使用陣列的方法:

  • 類陣列在數值讀取和設定、長度獲取、自身遍歷等方法用法相似。

  • 類陣列上不存在陣列中的操作方法。

  • 通過Function.call或者Function.apply方法改變this指向,可以間接在類陣列中使用陣列的方法。

  • 根據陣列中部分函式會返回一個新的陣列這一特性,我們可以實現類陣列到陣列的轉換或者實現陣列的快速拷貝。

  • 遍歷陣列的效率實際上比類陣列高很多,因此在遍歷一個類陣列的時候,建議優化的方式是先轉換成陣列再行遍歷需求。

由於水平有限,若有行文不全或疏漏錯誤之處,懇請各位讀者批評指正,一路有你,不勝感激!

感謝這個時代,讓我們可以站在巨人的肩膀上,窺探程式世界的巨集偉壯觀,我願以一顆赤子心,踏遍程式世界的千山萬水!願每一個行走在程式世界的同仁,都活成心中想要的樣子,加油

相關文章