深刻理解JavaScript類數組

原由

寫這篇博客的原由,是我在知乎上回答一個問題時,說本身在學前端時把《JavaScript高級程序設計》看了好幾遍。
因而在評論區中,出現了以下的對話:
對話javascript

天啦嚕,這話說的,寶寶感受到的,是滿滿的惡意啊。還好本身的JavaScript基礎還算不錯,沒被打臉。(吐槽一句:知乎少部分人真的是惡意度爆表,成天想着打別人的臉。都是搞技術的,和藹一點不行嗎…………)前端

不過這個話題也引發了個人注意,問了問身邊不少前端同窗關於數組與類數組的區別。他們都表示不太熟悉,因此決定寫一篇博客,來分享我對數組與類數組的理解。java

什麼是類數組

類數組的定義,有以下兩條:git

  • 具備:指向對象元素的數字索引下標以及 length 屬性告訴咱們對象的元素個數es6

  • 不具備:諸如 push 、 forEach 以及 indexOf 等數組對象具備的方法Qgithub

這兒有三個典型的JavaScript類數組例子。數組

1. DOM方法瀏覽器

// 獲取全部div
let arrayLike = document.querySelectorAll('div')

console.log(Object.prototype.toString.call(arrayLike))  // [object NodeList]

console.log(arrayLike.length) // 127

console.log(arrayLike[0]) 
// <div id="js-pjax-loader-bar" class="pjax-loader-bar"></div>

console.log(Array.isArray(arrayLike)) // false

arrayLike.push('push') 
// Uncaught TypeError: arrayLike.push is not a function(…)

是的,這個arrayLike的 NodeList,有length,也能用數組下標訪問,可是使用Array.isArray測試時,卻告訴咱們它不是數組。直接使用push方法時,固然也會報錯。
可是,咱們能夠借用類數組方法:app

let arr = Array.prototype.slice.call(arrayLike, 0)

console.log(Array.isArray(arr)) // true

arr.push('push something to arr')
console.log(arr[arr.length - 1]) // push something to arr

不難看出,此時的arrayLike在調用數組原型方法時,返回值已經轉化成數組了。也能正常使用數組的方法。函數

2. 類數組對象

let arrayLikeObj = {
  length: 2,
  0: 'This is Array Like Object',
  1: true
}

console.log(arrayLikeObj.length) // 2
console.log(arrayLikeObj[0]) // This is Array Like Object
console.log(Array.isArray(arrayLikeObj)) // false

let arrObj = Array.prototype.slice.call(arrayLikeObj, 0)
console.log(Array.isArray(arrObj)) // true

這個例子也很好理解。一個對象,加入了length屬性,再用Array的原型方法處理一下,搖身一變成爲了真的數組。

3. 類數組函數

這個應該算是最好玩,也是最迷惑人的類數組對象了。

let arrayLikeFunc1 = function () {}
console.log(arrayLikeFunc1.length) // 0
let arrFunc1 = Array.prototype.slice.call(arrayLikeFunc1, 0)
console.log(arrFunc1, arrFunc1.length) // ([], 0)

let arrayLikeFunc2 = function (a, b) {}
console.log(arrayLikeFunc2.length) // 2
let arrFunc2 = Array.prototype.slice.call(arrayLikeFunc2, 0)
console.log(arrFunc2, arrFunc2.length) // ([undefined × 2], 2)

能夠看出,函數也有length屬性,其值等於函數要接收的參數。

注:不適用於ES6的rest參數。具體緣由和表現這兒就再也不闡述了,不屬於本文討論範圍。可參見 《rest參數 - ECMAScript 6 入門》。另外arguments在ES6中,被rest參數代替了,因此這兒不做爲例子。

而length屬性大於0時,若是轉爲數組,則數組裏的值會是undefined。個數等於函數length的長度。

類數組的實現原理

類數組的實現原理,主要有如下兩點:
第一點是JavaScript的「萬物皆對象」概念。
第二點則是JavaScript支持的「鴨子類型」。

首先,從第一點開始解釋。

萬物皆對象

萬物皆對象具體解釋以下:

在JavaScript中,「一切皆對象」,數組和函數本質上都是對象,就連三種原始類型的值——數值、字符串、布爾值——在必定條件下,也會自動轉爲對象,也就是原始類型的「包裝對象」。

而另一個要點則是,全部對象都繼承於Object。因此都能調用對象的方法,好比使用點和方括號訪問屬性。
好比說,這樣的:

let func = function() {}
console.log(func instanceof Object) // true
func[0] = 'I\'m a func'
console.log(func[0]) // 'I\'m a func'

鴨子類型

萬物皆對象具體解釋以下:

若是它走起來像鴨子,並且叫起來像鴨子,那麼它就是鴨子。

好比說上面舉的類數組例子,雖然他們是對象/函數,可是隻要有length屬性和對應的數字下標,那麼他們就是數組。

可是,在這兒,仍是有些迷糊的。爲何使用call/apply借用數組方法就能處理這些類數組呢?

探祕V8

一開始,我也對這個犯迷糊啊。直到我去Github上,看到了谷歌V8引擎處理數組的源代碼。
地址在這兒:v8/array.js
做爲講述,咱們在這裏引用push的源代碼(方便講述,刪除部分。slice的比較長,可是原理一致):

// Appends the arguments to the end of the array and returns the new
// length of the array. See ECMA-262, section 15.4.4.7.
function ArrayPush() {
  // 獲取要處理的數組
  var array = TO_OBJECT(this);
  // 獲取數組長度
  var n = TO_LENGTH(array.length);
  // 獲取函數參數長度
  var m = arguments.length;

  for (var i = 0; i < m; i++) {
    // 將函數參數push進數組
    array[i+n] = arguments[i];
  }

  // 修正數組長度
  var new_length = n + m;
  array.length = new_length;
  // 返回值是數組的長度
  return new_length;
}

是的,整個push函數,並無涉及是不是數組的問題。只關心了length。而由於其對象的特性,因此可使用方括號來設置屬性。

這也是萬物皆類型和鴨子類型最生動的體現。

總結

JavaScript中的類數組的特殊性,是由其「萬物皆類型」和「鴨子類型」決定的,而瀏覽器引擎底層的實現,更是佐證了這一點。
而先前說個人那位同窗,由於只是知道類數組的幾種表現和用法,而且想經過apply來打我臉,證實我根本沒有仔細看書。這種行爲不只不友善,並且學習效率也不高。
由於,知其然而不知其因此然是不可取的。特別是發現不少這種例子,就得學會概括總結。(感謝winter老師的演講:一個前端的自我修養,教會我不少東西。)。
不少時候,深刻看看源代碼也會讓你對這個理解的更透徹。未來就算是蹦出一百種類數組,也能知道是怎麼回事兒。

最後,仍是開頭那句話:「都是搞技術的,和藹一點不行嗎?有問題就好好交流,不要總想着打別人臉啊…………」

最後附上本人博客地址和原文連接,但願能與各位多多交流。

Lxxyx的前端樂園
原文連接:深刻理解JavaScript類數組

相關文章
相關標籤/搜索