我在上一讲带你梳理了数组那令人眼花缭乱的各种方法,其实 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('jack', '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));
在这个有 form 表单的页面执行上面的代码,得到的结果如下。
可以看到,这里打印出来了页面第一个 form 表单元素,同时也打印出来了判断类型的结果,说明打印的判断的类型和 arguments 返回的也比较类似,typeof 返回的都是 ‘object’,和上面的类似。
另外需要注意的一点就是 HTML DOM 中的 HTMLCollection 是即时更新的,当其所包含的文档结构发生改变时,它会自动更新。下面我们再看最后一个 NodeList 类数组。
- NodeList
NodeList 对象是节点的集合,通常是由 querySlector 返回的。NodeList 不是一个数组,也是一种类数组。虽然 NodeList 不是一个数组,但是可以使用 for…of 来迭代。在一些情况下,NodeList 是一个实时集合,也就是说,如果文档中的节点树发生变化,NodeList 也会随之变化。我们还是利用代码来理解一下 Nodelist 这种类数组。
var list = document.querySelectorAll('input[type=checkbox]');
for (var checkbox of list) {
checkbox.checked = true;
}
console.log(list);
console.log(typeof list);
console.log(Object.prototype.toString.call(list));
从上面的代码执行的结果中可以发现,我们是通过有 CheckBox 的页面执行的代码,在结果可中输出了一个 NodeList 类数组,里面有一个 CheckBox 元素,并且我们判断了它的类型,和上面的 arguments 与 HTMLCollection 其实是类似的,执行结果如下图所示。
好了,现在你已经了解了上面这三种类数组,那么结合这些,我们再看看类数组有哪些应用的场景呢?
类数组应用场景
我在这里为你介绍三种场景,这些也是最常见的。
遍历参数操作
我们在函数内部可以直接获取 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"
这段代码说明了,你可以传递任意数量的参数到该函数,并使用每个参数作为列表中的项创建列表进行拼接。从这个例子中也可以看出,我们可以在日常编码中采用这样的代码抽象方式,把需要解决的这一类问题,都抽象成通用的方法,来提升代码的可复用性。
下面我们再看另外一种使用场景。
传递参数使用
我在第 4 讲中已经介绍过了 apply 和 call 的使用访问,结合这一讲的内容,我们来研究一下,如果再结合 arguments,还能实现什么?可以借助 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 这样的函数可以灵活传入参数数量,通过这样的代码编写方式是不是也可以实现一些功能的拓展场景呢?
如何将类数组转换成数组
在互联网大厂的面试中,类数组转换成数组这样的题目经常会问,也会问你 arguments 的相关问题,那么结合本讲的内容,下面我带你思考一下如何将类数组转换为数组。大致的实现方式有两种,我将依次讲解。
类数组借用数组方法转数组
apply 和 call 方法之前我们有详细讲过,类数组因为不是真正的数组,所以没有数组类型上自带的那些方法,我们就需要利用下面这几个方法去借用数组的方法。比如借用数组的 push 方法,请看下面的一段代码。
var arrayLike = {
0: 'java',
1: 'script',
length: 2
}
Array.prototype.push.call(arrayLike, 'jack', 'lily');
console.log(typeof arrayLike); // 'object'
console.log(arrayLike);
// {0: "java", 1: "script", 2: "jack", 3: "lily", 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 的展开运算符,虽然写法不一样,但是基本都可以满足多个参数实现累加的效果。
讲到这里你可以再思考一下这两种类数组转换数组的方法,是不是很好理解呢?
总结
在这一讲中,我把日常开发中有可能遇到的几种类数组分别介绍了一遍,又结合类数组相关的应用场景进行了全方位的讲解,类数组应用场景的几个例子希望能为你的 JS 编码能力的提升带来帮助和启发。最后我又讲了类数组转换成数组的两种方式。
综上你可以看到,类数组这节课的知识点与第 4 讲中 apply、call 还是有紧密联系的,你可以通过下面的表格再重新梳理一下类数组和数组的异同点。
在前端工作中,开发者往往会忽视对类数组的学习,其实在高级 JavaScript 编程中经常需要将类数组向数组转化,尤其是一些比较复杂的开源项目,经常会看到函数中处理参数的写法,例如:[].slice.call(arguments) 这行代码。
通过本讲的学习,希望你以后在阅读别人代码的时候,能更清楚地理解高手们处理类数组的逻辑,以便在面试或者编码中能够轻松应对。
下一讲,我们来聊聊数组扁平化的相关内容,欢迎提前预习,届时我们再好好讨论。