其实 JS 中一直存在一种类数组的对象,它们不能直接调用数组的方法,但是又和数组比较类似,在某些特定的编程场景中会出现,这会让很多 JS 的初学者比较困惑。
先来看看在 JavaScript
中有哪些情况下的对象是类数组呢?主要有以下几种:
- 函数里面的参数对象
arguments
; - 用
getElementsByTagName/ClassName/Name
获得的HTMLCollection
; - 用
querySelector
获得的NodeList
。
上述这些基本就是在 JavaScript
编程过程中经常会遇到的。
在开始前请先思考几个问题:
- 类数组是否能使用数组的方法呢?
- 类数组有哪些方式可以转换成数组?
类数组基本介绍
arguments
先来重点讲讲 arguments
对象,我们在日常开发中经常会遇到各种类数组对象,最常见的便是在函数中使用的 arguments
,它的对象只定义在函数体中,包括了函数的参数和其他属性。我们通过一段代码来看下 arguments
的使用方法,如下所示。
function foo(name, age, sex) {
console.log(arguments);
console.log(typeof arguments);
console.log(Object.prototype.toString.call(arguments));
}
foo('mark', '18', 'male');
这段代码比较容易,就是直接将这个函数的 arguments
在函数内部打印出来,那么我们看下这个 arguments
打印出来的结果,请看控制台的这张截图。
从结果中可以看到,typeof
这个 arguments
返回的是 object
,通过 Object.prototype.toString.call
返回的结果是 '[object arguments]'
,可以看出来返回的不是 '[object array]'
,说明 arguments
和数组还是有区别的。
length
属性很好理解,它就是函数参数的长度,我们从打印出的代码也可以看得出来。另外可以看到 arguments
不仅仅有一个 length
属性,还有一个 callee
属性,我们接下来看看这个 callee
是干什么的,代码如下所示。
function foo(name, age, sex) {
console.log(arguments.callee);
}
foo('jack', '18', 'male');
请看这段代码的执行结果。
从控制台可以看到,输出的就是函数自身,如果在函数内部直接执行调用 callee 的话,那它就会不停地执行当前函数,直到执行到内存溢出
HTMLCollection
HTMLCollection
简单来说是 HTML DOM
对象的一个接口,这个接口包含了获取到的 DOM
元素集合,返回的类型是类数组对象,如果用 typeof
来判断的话,它返回的是'object'
。它是及时更新的,当文档中的 DOM
变化时,它也会随之变化。
描述起来比较抽象,还是通过一段代码来看下 HTMLCollection
最后返回的是什么,我们先随便找一个页面中有 form
表单的页面,在控制台中执行下述代码。
var elem1, elem2;
// document.forms 是一个 HTMLCollection
elem1 = document.forms[0];
elem2 = document.forms.item(0);
console.log(elem1);
console.log(elem2);
console.log(typeof elem1);
console.log(Object.prototype.toString.call(elem1));
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Nl1azVq-1635412164265)(C:\Users\JYcx\AppData\Roaming\Typora\typora-user-images\image-20211028164827915.png)]
可以看到,这里打印出来了页面第一个 form
表单元素,同时也打印出来了判断类型的结果,说明打印的判断的类型和 arguments
返回的也比较类似,typeof
返回的都是 ‘object
’,和上面的类似。
另外需要注意的一点就是 HTML DOM
中的 HTMLCollection
是即时更新的,当其所包含的文档结构发生改变时,它会自动更新。
NodeList
NodeList
对象是节点的集合,通常是由 querySlector
返回的。NodeList
不是一个数组,也是一种类数组。虽然 NodeList
不是一个数组,但是可以使用 for...of
来迭代。在一些情况下,NodeList
是一个实时集合,也就是说,如果文档中的节点树发生变化,NodeList
也会随之变化。还是利用代码来理解一下 Nodelist
这种类数组。
var list = document.querySelectorAll('input[type=radio]');
for (var checkbox of list) {
checkbox.checked = true;
}
console.log(list);
console.log(typeof list);
console.log(Object.prototype.toString.call(list));
从上面的代码执行的结果中可以发现,我们是通过有 ·radio的页面执行的代码,在结果可中输出了一个 NodeList
类数组,里面有一个 radio
元素,并且我们判断了它的类型,和上面的 arguments
与 HTMLCollection
其实是类似的,执行结果如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MCT59297-1635412164270)(C:\Users\JYcx\AppData\Roaming\Typora\typora-user-images\image-20211028165150859.png)]
类数组应用场景
遍历参数操作
在函数内部可以直接获取 arguments
这个类数组的值,那么也可以对于参数进行一些操作,比如下面这段代码,可以将函数的参数默认进行求和操作。
function add() {
var sum =0,
len = arguments.length;
for(var i = 0; i < len; i++){
sum += arguments[i];
}
return sum;
}
add() // 0
add(1) // 1
add(1,2) // 3
add(1,2,3,4); // 10
结合上面这段代码,在函数内部可以将参数直接进行累加操作,以达到预期的效果,参数多少也可以不受限制,根据长度直接计算,返回出最后函数的参数的累加结果,其他的操作也都可以仿照这样的方式来做。
定义链接字符串函数
我们可以通过 arguments
这个例子定义一个函数来连接字符串。这个函数唯一正式声明了的参数是一个字符串,该参数指定一个字符作为衔接点来连接字符串。该函数定义如下。
function myConcat(separa) {
var args = Array.prototype.slice.call(arguments, 1);
return args.join(separa);
}
myConcat(", ", "red", "orange", "blue");
// "red, orange, blue"
myConcat("; ", "elephant", "lion", "snake");
// "elephant; lion; snake"
myConcat(". ", "one", "two", "three", "four", "five");
// "one. two. three. four. five"
这段代码说明了,可以传递任意数量的参数到该函数,并使用每个参数作为列表中的项创建列表进行拼接。从这个例子中也可以看出,可以在日常编码中采用这样的代码抽象方式,把需要解决的这一类问题,都抽象成通用的方法,来提升代码的可复用性。
传递参数使用
借助 arguments
将参数从一个函数传递到另一个函数,请看下面这个例子。
// 使用 apply 将 foo 的参数传递给 bar
function foo() {
bar.apply(this, arguments);
}
function bar(a, b, c) {
console.log(a, b, c);
}
foo(1, 2, 3) //1 2 3
上述代码中,通过在 foo
函数内部调用 apply
方法,用 foo
函数的参数传递给 bar
函数,这样就实现了借用参数的妙用。你可以结合这个例子再思考一下,对于 foo
这样的函数可以灵活传入参数数量,通过这样的代码编写方式是不是也可以实现一些功能的拓展场景呢?
如何将类数组转换成数组
类数组借用数组方法转数组
apply
和 call
方法之前我们有详细讲过,类数组因为不是真正的数组,所以没有数组类型上自带的那些方法,就需要利用下面这几个方法去借用数组的方法。比如借用数组的 push
方法,请看下面的一段代码。
var arrayLike = {
0: 'java',
1: 'script',
length: 2
}
Array.prototype.push.call(arrayLike, 'mark', 'eric');
console.log(typeof arrayLike); // 'object'
console.log(arrayLike);
// {0: "java", 1: "script", 2: "mark", 3: "eric", length: 4}
从中可以看到,arrayLike
其实是一个对象,模拟数组的一个类数组,从数据类型上说它是一个对象,新增了一个 length
的属性。从代码中还可以看出,用 typeof
来判断输出的是 'object'
,它自身是不会有数组的 push
方法的,这里我们就用 call
的方法来借用 Array
原型链上的 push
方法,可以实现一个类数组的 push
方法,给 arrayLike
添加新的元素。
从控制台的结果可以看出,数组的 push
方法满足了我们想要实现添加元素的诉求。再来看下 arguments
如何转换成数组,请看下面这段代码。
function sum(a, b) {
let args = Array.prototype.slice.call(arguments);
// let args = [].slice.call(arguments); // 这样写也是一样效果
console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2); // 3
function sum(a, b) {
let args = Array.prototype.concat.apply([], arguments);
console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2); // 3
这段代码中可以看到,还是借用 Array
原型链上的各种方法,来实现 sum
函数的参数相加的效果。一开始都是将 arguments
通过借用数组的方法转换为真正的数组,最后都又通过数组的 reduce
方法实现了参数转化的真数组 args
的相加,最后返回预期的结果。
ES6 的方法转数组
对于类数组转换成数组的方式,我们还可以采用 ES6
新增的 Array.from
方法以及展开运算符的方法。那么还是围绕上面这个 sum
函数来进行改变,看下用 Array.from
和展开运算符是怎么实现转换数组的,请看下面一段代码的例子。
function sum(a, b) {
let args = Array.from(arguments);
console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2); // 3
function sum(a, b) {
let args = [...arguments];
console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2); // 3
function sum(...args) {
console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2); // 3
从代码中可以看出,Array.from
和 ES6
的展开运算符,都可以把 arguments
这个类数组转换成数组 args
,从而实现调用 reduce
方法对参数进行累加操作。其中第二种和第三种都是用 ES6
的展开运算符,虽然写法不一样,但是基本都可以满足多个参数实现累加的效果。
总结
可以看到,类数组这节课的知识点与 apply
、call
还是有紧密联系的,你通过下面的表格再重新梳理一下类数组和数组的异同点。
方法特征 | 数组 | 类数组 |
---|---|---|
自带方法 | 多个参数 | 无 |
length属性 | 有 | 有 |
calles属性 | 无 | 有 |