0%

类数组对象转数组

引言

ES中有的对象长得很像数组,却又不是数组,比如函数中的arguments对象。如果我们想把这类对象转换为数组对象,就要用到我们今天要说的方法了。但是在这之前,需要先弄清楚几个概念:

  • 类数组对象
  • arguments
  • 数组的slice方法
  • call和apply

正文

类数组对象与arguments对象

我们先搞明白什么是arguments对象,类数组对象是怎么回事也就明白了。

在Chrome浏览器的控制台里直接打印arguments的结果是这样的:

报错:arguments is not defined。这是因为arguments对象是依附于函数存在的,用来储存所有传递进函数的参数。

知道了这一点,我们再在函数里打印arguments看看。

如果不传参数调用函数foo可以发现打印结果里foo的arguments里有一个length属性,值为0。还有一个callee属性,这是一个指针,指向函数本身,这里暂时不讨论它。把这个arguments对象简单的表示一下就是(忽略callee,减少干扰因素):

1
{length: 0}

如果给函数传递一些参数,foo(1,2)调用的结果又会是怎样的呢?

可以发现,length属性的值变成了2,也就是传入参数的个数。并且另外多了两个属性,属性名分别为0和1,属性值分别为1和2。简单表示一下就是:

1
{0: 1, 1: 2, length: 2}

由于arguments对象的属性名跟数组中的索引很像,而且它也有length属性,访问arguments对象的属性也是这种方式arguments[0],总之各方面都和数组很像,所以把它叫做类数组对象。但是要知道它并不是Array类型的实例,它并没有Array的很多原型方法(比如slice,splice,concat…),Array也没有callee属性。arguments instanceof Array === false

另外,我测试了,不管是数组还是arguments,其索引属性都是字符串,而不是数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo(){
var arr = null;
for(var prop in arguments){
console.log(prop,typeof prop);
}
}
foo(1,2); // 0 string 1 string

function bar(){
var arr = [1,2];
for(var prop in arr){
console.log(prop,typeof prop);
}
}
bar(1,2); // 0 string 1 string

但是,数字和字符串的访问形式都是可以的。

1
2
3
var arr = [1,2];
arr[0]; // 1
arr['0']; // 1

数组的slice方法

在正式的开始实现转换方法之前,我们要先了解一下Array类型的slice方法。

slice方法接收两个参数,根据这两个参数来截取当前数组,返回一个新的数组,原数组不变。

1
2
3
4
5
var arr = [1,2,3];
arr.slice(0,1); // [1]
arr.slice(0,2); // [1,2]
arr.slice(0,3); // [1,2,3]
arr.slice(); // [1,2,3]

规则是截取两个参数的索引之间的项,组成一个新数组(第二个参数对应的项不会被截取,也就是说只会截取到第二个参数对应的项的前一个项)。

如果什么参数都不传的话,就相当于对当前数组的一个复制操作。

  • 不传参数的情况,相当于arr.slice(0,arr.length)
  • 参数为负数时,会把这个参数与length相加再计算。以上面的arr为例,arr.slice(-1,3)相当于arr.slice(2,3);如果相加后仍为负数,则把这个参数当做0来算。
  • 如果第二个参数小于第一个参数,返回空数组。

call和apply

call和apply其实我觉得可以拎出来用整片篇幅来说的。简单来说就是:

  • call和apply是每个函数都有的方法。
  • call和apply可以用来让函数在指定的作用域中执行。
  • call和apply的第一个参数都是用来指定this的,即上一条说的我们期望函数执行所在的作用域。
  • 它们唯一的不同就是:除了第一个参数外,apply接收一个数组或arguments作为参数,作为传入函数的参数。而call是直接接收一个个单独的参数,作为传入函数的参数。

还是看一个简单的例子。

1
2
3
4
5
6
7
var a = 10;
var o = { a: 20};
function foo(){
console.log(this.a);
}
foo(); // 10
foo.call(o); // 20

上面的例子中,通过函数foo可以访问this.a。直接调用函数时,this为window对象,访问到的是作为window的属性的全局变量a。通过call把foo的this绑定在对象o上后,this.a访问到的就是对象o中的属性a了。

开始实现

了解了上面的基础,接下来的实现才会更好理解。先上代码

1
2
3
4
5
function foo(){
return Array.prototype.slice.call(arguments);
}

foo(1,2,3); // [1,2,3]

上面的示例代码就把函数的arguments对象转换为数组返回了。我一开始不知道为什么这样就能把arguments转换为数组了,今天整理的时候想了一下,slice底层是怎么实现这个功能的呢?然后试着模拟了一下,发现把这个过程重演更有利于理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 模拟slice实现

Array.prototype.slice = function(a,b){
var arr = new Array();
// 这里简化了,省略了参数为负以及第二个参数小于第一个参数情况下的判断
a = a || 0;
b = ((b < this.length) ? b : this.length);

for(var i = a; i < b; i++){
arr.push(this[i]);
}

return arr;
}

可以看到上面模拟出来的过程就是创建一个新数组,然后把用于截取的当前数组(对应上面代码中的this)中的项塞进新数组,然后返回新数组。那Array.prototype.slice.call(arguments);的过程就相当于只是把当前数组换成了arguments。这样整个过程就清晰了。

再把上面的foo封装一下,使得它可以适用于所有的类数组对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
function toArr(s) {
var arr = null;
try {
arr = Array.prototype.slice.call(s); // >IE8
} catch(ex) {
arr = new Array();
for(var i = 0,len = s.length; i < len; i++){
arr.push(s[i]);
}
}

return arr;
}

上面的代码实际上是《JavaScript高级程序设计》第十章中用来把nodelist对象转换为数组用到的方法。用try…catch来封装,是为了兼容IE8及之前的浏览器。下面是摘抄的原话。

由于IE8及更早版本将NodeList实现为一个COM对象,而我们不能像使用JScript对象那样使用这种对象,因此上面的代码会导致错误。要想在IE中将NodeList转换为数组,必须手动枚举所有成员。

调用的时候,直接把类数组对象传入函数并执行就好了,我们来看一个例子。

1
2
3
4
5
6
7
var arrlike1 = {
0: 1,
1: 2,
length: 2
};
console.log(toArr(arrlike1)); // [1,2]
console.log(toArr(arrlike1) instanceof Array); // true

上面的例子中声明了一个类数组对象,用toArr转换后打印的结果就是我们预期中的数组。

需要特别注意的一点是:类数组对象必须包含length属性和正确的length属性值。如果不包含length属性,则这个对象会被转换为一个空数组;如果length属性的值不正确,则转换的结果会根据length值增加或删除数组的项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var arrlike2 = {
0:1,
1:2
};
var arrlike3 = {
0:1,
1,2,
length: 1
}
var arrlike4 = {
0:1,
1,2,
length: 3
}

toArr(arrlike2); // []
toArr(arrlike3); // [1]
toArr(arrlike4); // [1,2,undefined]

另外,把nodelist对象转换为数组的姿势是这样的:

1
2
var list = document.getElementsByTagName('li');
var arr = toArr(list);

好了,就酱。