JavaScript 核心原理精讲 - 前美团前端技术专家 - 拉勾教育

我在上一讲带你梳理了数组那令人眼花缭乱的各种方法,其实 JS 中一直存在一种类数组的对象,它们不能直接调用数组的方法,但是又和数组比较类似,在某些特定的编程场景中会出现,这会让很多 JS 的初学者比较困惑。那么通过本讲的学习,希望你能更好地去理解类数组原理,以及类数组的转换。

我们先来看看在 JavaScript 中有哪些情况下的对象是类数组呢?主要有以下几种:

  1. 函数里面的参数对象 arguments;
  2. 用 getElementsByTagName/ClassName/Name 获得的 HTMLCollection;
  3. 用 querySelector 获得的 NodeList。

上述这些基本就是你在 JavaScript 编程过程中经常会遇到的,希望通过这一讲,你能够彻底掌握它们。

在课程开始前请你先思考几个问题:

  1. 类数组是否能使用数组的方法呢?
  2. 类数组有哪些方式可以转换成数组?

如果你不能准确地回答出这两个问题,那么就要认真学习了,下面开始进入我们的课程。

类数组基本介绍

arguments

先来重点讲讲 arguments 对象,我们在日常开发中经常会遇到各种类数组对象,最常见的便是在函数中使用的 arguments,它的对象只定义在函数体中,包括了函数的参数和其他属性。我们通过一段代码来看下 arguments 的使用方法,如下所示。

  1. function foo(name, age, sex) {
  2. console.log(arguments);
  3. console.log(typeof arguments);
  4. console.log(Object.prototype.toString.call(arguments));
  5. }
  6. foo('jack', '18', 'male');

这段代码比较容易,就是直接将这个函数的 arguments 在函数内部打印出来,那么我们看下这个 arguments 打印出来的结果,请看控制台的这张截图。

08 | 数组原理(中):如何理解 JS 的类数组? - 图1

从结果中可以看到,typeof 这个 arguments 返回的是 object,通过 Object.prototype.toString.call 返回的结果是 ‘[object arguments]’,可以看出来返回的不是’[object array]’,说明 arguments 和数组还是有区别的。

length 属性很好理解,它就是函数参数的长度,我们从打印出的代码也可以看得出来。另外可以看到 arguments 不仅仅有一个 length 属性,还有一个 callee 属性,我们接下来看看这个 callee 是干什么的,代码如下所示。

  1. function foo(name, age, sex) {
  2. console.log(arguments.callee);
  3. }
  4. foo('jack', '18', 'male');

请看这段代码的执行结果。

08 | 数组原理(中):如何理解 JS 的类数组? - 图2

从控制台可以看到,输出的就是函数自身,如果在函数内部直接执行调用 callee 的话,那它就会不停地执行当前函数,直到执行到内存溢出,有兴趣的话你可以自己试一下。

接下来我们再看看下面这种类数组。

HTMLCollection

HTMLCollection 简单来说是 HTML DOM 对象的一个接口,这个接口包含了获取到的 DOM 元素集合,返回的类型是类数组对象,如果用 typeof 来判断的话,它返回的是’object’。它是及时更新的,当文档中的 DOM 变化时,它也会随之变化。

描述起来比较抽象,还是通过一段代码来看下 HTMLCollection 最后返回的是什么,我们先随便找一个页面中有 form 表单的页面,在控制台中执行下述代码。

  1. var elem1, elem2;
  2. elem1 = document.forms[0];
  3. elem2 = document.forms.item(0);
  4. console.log(elem1);
  5. console.log(elem2);
  6. console.log(typeof elem1);
  7. console.log(Object.prototype.toString.call(elem1));

在这个有 form 表单的页面执行上面的代码,得到的结果如下。

08 | 数组原理(中):如何理解 JS 的类数组? - 图3

可以看到,这里打印出来了页面第一个 form 表单元素,同时也打印出来了判断类型的结果,说明打印的判断的类型和 arguments 返回的也比较类似,typeof 返回的都是’object’,和上面的类似。

另外需要注意的一点就是 HTML DOM 中的 HTMLCollection 是即时更新的,当其所包含的文档结构发生改变时,它会自动更新。下面我们再看最后一个 NodeList 类数组。

NodeList

NodeList 对象是节点的集合,通常是由 querySlector 返回的。NodeList 不是一个数组,也是一种类数组。虽然 NodeList 不是一个数组,但是可以使用 for…of 来迭代。在一些情况下,NodeList 是一个实时集合,也就是说,如果文档中的节点树发生变化,NodeList 也会随之变化。我们还是利用代码来理解一下 Nodelist 这种类数组。

  1. var list = document.querySelectorAll('input[type=checkbox]');
  2. for (var checkbox of list) {
  3. checkbox.checked = true;
  4. }
  5. console.log(list);
  6. console.log(typeof list);
  7. console.log(Object.prototype.toString.call(list));

从上面的代码执行的结果中可以发现,我们是通过有 CheckBox 的页面执行的代码,在结果可中输出了一个 NodeList 类数组,里面有一个 CheckBox 元素,并且我们判断了它的类型,和上面的 arguments 与 HTMLCollection 其实是类似的,执行结果如下图所示。

08 | 数组原理(中):如何理解 JS 的类数组? - 图4

好了,现在你已经了解了上面这三种类数组,那么结合这些,我们再看看类数组有哪些应用的场景呢?

类数组应用场景

我在这里为你介绍三种场景,这些也是最常见的。

遍历参数操作

我们在函数内部可以直接获取 arguments 这个类数组的值,那么也可以对于参数进行一些操作,比如下面这段代码,我们可以将函数的参数默认进行求和操作。

  1. function add() {
  2. var sum =0,
  3. len = arguments.length;
  4. for(var i = 0; i < len; i++){
  5. sum += arguments[i];
  6. }
  7. return sum;
  8. }
  9. add()
  10. add(1)
  11. add(12)
  12. add(1,2,3,4);

结合上面这段代码,我们在函数内部可以将参数直接进行累加操作,以达到预期的效果,参数多少也可以不受限制,根据长度直接计算,返回出最后函数的参数的累加结果,其他的操作也都可以仿照这样的方式来做。我们再看下一种场景。

定义链接字符串函数

我们可以通过 arguments 这个例子定义一个函数来连接字符串。这个函数唯一正式声明了的参数是一个字符串,该参数指定一个字符作为衔接点来连接字符串。该函数定义如下。

  1. function myConcat(separa) {
  2. var args = Array.prototype.slice.call(arguments, 1);
  3. return args.join(separa);
  4. }
  5. myConcat(", ", "red", "orange", "blue");
  6. myConcat("; ", "elephant", "lion", "snake");
  7. myConcat(". ", "one", "two", "three", "four", "five");

这段代码说明了,你可以传递任意数量的参数到该函数,并使用每个参数作为列表中的项创建列表进行拼接。从这个例子中也可以看出,我们可以在日常编码中采用这样的代码抽象方式,把需要解决的这一类问题,都抽象成通用的方法,来提升代码的可复用性。

下面我们再看另外一种使用场景。

传递参数使用

我在第 4 讲中已经介绍过了 apply 和 call 的使用访问,结合这一讲的内容,我们来研究一下,如果再结合 arguments,还能实现什么?可以借助 arguments 将参数从一个函数传递到另一个函数,请看下面这个例子。

  1. function foo() {
  2. bar.apply(this, arguments);
  3. }
  4. function bar(a, b, c) {
  5. console.log(a, b, c);
  6. }
  7. foo(1, 2, 3)

上述代码中,通过在 foo 函数内部调用 apply 方法,用 foo 函数的参数传递给 bar 函数,这样就实现了借用参数的妙用。你可以结合这个例子再思考一下,对于 foo 这样的函数可以灵活传入参数数量,通过这样的代码编写方式是不是也可以实现一些功能的拓展场景呢?

如何将类数组转换成数组

在互联网大厂的面试中,类数组转换成数组这样的题目经常会问,也会问你 arguments 的相关问题,那么结合本讲的内容,下面我带你思考一下如何将类数组转换为数组。大致的实现方式有两种,我将依次讲解。

类数组借用数组方法转数组

apply 和 call 方法之前我们有详细讲过,类数组因为不是真正的数组,所以没有数组类型上自带的那些方法,我们就需要利用下面这几个方法去借用数组的方法。比如借用数组的 push 方法,请看下面的一段代码。

  1. var arrayLike = {
  2. 0: 'java',
  3. 1: 'script',
  4. length: 2
  5. }
  6. Array.prototype.push.call(arrayLike, 'jack', 'lily');
  7. console.log(typeof arrayLike);
  8. console.log(arrayLike);

从中可以看到,arrayLike 其实是一个对象,模拟数组的一个类数组,从数据类型上说它是一个对象,新增了一个 length 的属性。从代码中还可以看出,用 typeof 来判断输出的是’object’,它自身是不会有数组的 push 方法的,这里我们就用 call 的方法来借用 Array 原型链上的 push 方法,可以实现一个类数组的 push 方法,给 arrayLike 添加新的元素。

从控制台的结果可以看出,数组的 push 方法满足了我们想要实现添加元素的诉求。我们再来看下 arguments 如何转换成数组,请看下面这段代码。

  1. function sum(a, b) {
  2. let args = Array.prototype.slice.call(arguments);
  3. console.log(args.reduce((sum, cur) => sum + cur));
  4. }
  5. sum(1, 2);
  6. function sum(a, b) {
  7. let args = Array.prototype.concat.apply([], arguments);
  8. console.log(args.reduce((sum, cur) => sum + cur));
  9. }
  10. sum(1, 2);

这段代码中可以看到,还是借用 Array 原型链上的各种方法,来实现 sum 函数的参数相加的效果。一开始都是将 arguments 通过借用数组的方法转换为真正的数组,最后都又通过数组的 reduce 方法实现了参数转化的真数组 args 的相加,最后返回预期的结果。

ES6 的方法转数组

对于类数组转换成数组的方式,我们还可以采用 ES6 新增的 Array.from 方法以及展开运算符的方法。那么还是围绕上面这个 sum 函数来进行改变,我们看下用 Array.from 和展开运算符是怎么实现转换数组的,请看下面一段代码的例子。

  1. function sum(a, b) {
  2. let args = Array.from(arguments);
  3. console.log(args.reduce((sum, cur) => sum + cur));
  4. }
  5. sum(1, 2);
  6. function sum(a, b) {
  7. let args = [...arguments];
  8. console.log(args.reduce((sum, cur) => sum + cur));
  9. }
  10. sum(1, 2);
  11. function sum(...args) {
  12. console.log(args.reduce((sum, cur) => sum + cur));
  13. }
  14. sum(1, 2);

从代码中可以看出,Array.from 和 ES6 的展开运算符,都可以把 arguments 这个类数组转换成数组 args,从而实现调用 reduce 方法对参数进行累加操作。其中第二种和第三种都是用 ES6 的展开运算符,虽然写法不一样,但是基本都可以满足多个参数实现累加的效果。

讲到这里你可以再思考一下这两种类数组转换数组的方法,是不是很好理解呢?

总结

在这一讲中,我把日常开发中有可能遇到的几种类数组分别介绍了一遍,又结合类数组相关的应用场景进行了全方位的讲解,类数组应用场景的几个例子希望能为你的 JS 编码能力的提升带来帮助和启发。最后我又讲了类数组转换成数组的两种方式。

综上你可以看到,类数组这节课的知识点与第 4 讲中 apply、call 还是有紧密联系的,你可以通过下面的表格再重新梳理一下类数组和数组的异同点。

08 | 数组原理(中):如何理解 JS 的类数组? - 图5

在前端工作中,开发者往往会忽视对类数组的学习,其实在高级 JavaScript 编程中经常需要将类数组向数组转化,尤其是一些比较复杂的开源项目,经常会看到函数中处理参数的写法,例如:[].slice.call(arguments) 这行代码。

通过本讲的学习,希望你以后在阅读别人代码的时候,能更清楚地理解高手们处理类数组的逻辑,以便在面试或者编码中能够轻松应对。

下一讲,我们来聊聊数组扁平化的相关内容,欢迎提前预习,届时我们再好好讨论。