深入理解JavaScript類數組
寫這篇博客的起因,是我在知乎上回答一個問題時,說自己在學前端時把《JavaScript高級程序設計》看了好幾遍。
于是在評論區中,出現了如下的對話:
天啦嚕,這話說的,寶寶感覺到的,是滿滿的惡意啊。還好自己的JavaScript基礎還算不錯,沒被打臉。(吐槽一句:知乎少部分人真的是惡意度爆表,整天想著打別人的臉。都是搞技術的,和善一點不行嗎…………)
不過這個話題也引起了我的注意,問了問身邊很多前端同學關于數組與類數組的區別。他們都表示不太熟悉,所以決定寫一篇博客,來分享我對數組與類數組的理解。
什么是類數組
類數組的定義,有如下兩條:
- 具有:指向對象元素的數字索引下標以及 length 屬性告訴我們對象的元素個數
- 不具有:諸如 push 、 forEach 以及 indexOf 等數組對象具有的方法Q
這兒有三個典型的JavaScript類數組例子。
- 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方法時,當然也會報錯。
但是,我們可以借用類數組方法:
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在調用數組原型方法時,返回值已經轉化成數組了。也能正常使用數組的方法。
- 類數組對象
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的原型方法處理一下,搖身一變成為了真的數組。
- 類數組函數
這個應該算是最好玩,也是最迷惑人的類數組對象了。
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(arrayLikeFunc1.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老師的演講: 一個前端的自我修養 ,教會我很多東西。)。
很多時候,深入看看源代碼也會讓你對這個理解的更透徹。將來就算是蹦出一百種類數組,也能知道是怎么回事兒。
最后,還是開頭那句話:“都是搞技術的,和善一點不行嗎?有問題就好好交流,不要總想著打別人臉啊…………”
來自: http://www.lxxyx.win/2016/05/07/深入理解JavaScript類數組/