五分钟快速了解ArrayLike(类数组)

3,397

我在从性能优化角度探讨浏览器重绘与重排的过程一文中提到过采用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指向,可以间接在类数组中使用数组的方法。

  • 根据数组中部分函数会返回一个新的数组这一特性,我们可以实现类数组到数组的转换或者实现数组的快速拷贝。

  • 遍历数组的效率实际上比类数组高很多,因此在遍历一个类数组的时候,建议优化的方式是先转换成数组再行遍历需求。

由于水平有限,若有行文不全或疏漏错误之处,恳请各位读者批评指正,一路有你,不胜感激!

感谢这个时代,让我们可以站在巨人的肩膀上,窥探程序世界的宏伟壮观,我愿以一颗赤子心,踏遍程序世界的千山万水!愿每一个行走在程序世界的同仁,都活成心中想要的样子,加油