引言
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 | function foo(){ |
但是,数字和字符串的访问形式都是可以的。
1 | var arr = [1,2]; |
数组的slice方法
在正式的开始实现转换方法之前,我们要先了解一下Array类型的slice方法。
slice方法接收两个参数,根据这两个参数来截取当前数组,返回一个新的数组,原数组不变。
1 | var arr = [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 | var a = 10; |
上面的例子中,通过函数foo可以访问this.a
。直接调用函数时,this为window对象,访问到的是作为window的属性的全局变量a。通过call把foo的this绑定在对象o上后,this.a
访问到的就是对象o中的属性a了。
开始实现
了解了上面的基础,接下来的实现才会更好理解。先上代码
1 | function foo(){ |
上面的示例代码就把函数的arguments对象转换为数组返回了。我一开始不知道为什么这样就能把arguments转换为数组了,今天整理的时候想了一下,slice底层是怎么实现这个功能的呢?然后试着模拟了一下,发现把这个过程重演更有利于理解。
1 | // 模拟slice实现 |
可以看到上面模拟出来的过程就是创建一个新数组,然后把用于截取的当前数组(对应上面代码中的this)中的项塞进新数组,然后返回新数组。那Array.prototype.slice.call(arguments);
的过程就相当于只是把当前数组换成了arguments。这样整个过程就清晰了。
再把上面的foo封装一下,使得它可以适用于所有的类数组对象。
1 | function toArr(s) { |
上面的代码实际上是《JavaScript高级程序设计》第十章中用来把nodelist对象转换为数组用到的方法。用try…catch来封装,是为了兼容IE8及之前的浏览器。下面是摘抄的原话。
由于IE8及更早版本将NodeList实现为一个COM对象,而我们不能像使用JScript对象那样使用这种对象,因此上面的代码会导致错误。要想在IE中将NodeList转换为数组,必须手动枚举所有成员。
调用的时候,直接把类数组对象传入函数并执行就好了,我们来看一个例子。
1 | var arrlike1 = { |
上面的例子中声明了一个类数组对象,用toArr转换后打印的结果就是我们预期中的数组。
需要特别注意的一点是:类数组对象必须包含length属性和正确的length属性值。如果不包含length属性,则这个对象会被转换为一个空数组;如果length属性的值不正确,则转换的结果会根据length值增加或删除数组的项。
1 | var arrlike2 = { |
另外,把nodelist对象转换为数组的姿势是这样的:
1 | var list = document.getElementsByTagName('li'); |
好了,就酱。