我们什么时候会遇到类数组?

96 阅读3分钟

作为一名前端程序员,我们往往对于数组的各种API都能熟练运用,举一反三,但是对于类数组我们却了解甚少,或许是在业务代码中比较少见。类数组和数组比较相似,在一些特定开发场景中会出现,会让一些初学者感到困惑,有时甚至职场老鸟也只是知道有这么个东西,不了解一些具体的细节。博主由于偶然间突然兴起探究,如有不正确之处,还请指教。

类数组的定义

我们在调试打印的时候,偶尔会遇到一些跟数组结构很像的“数组”,它们有中括号[],有数值索引,有对应值,能读取长度,甚至有Iterator接口,但却不能使用Array的原生方法,如push、map等,这样的一种“数组”我们称之为类数组,也可以说是类数组对象。

哪里会遇到遇到类数组?

普通函数(箭头函数是没有arguments的)

普通函数里的arguments是类数组最常见的一种场景,它包含了函数的参数和一些其它的属性,我们通过代码来看一下arguments的结构:

function test(name, age, sex) {
    console.log(arguments)
    console.log(typeof arguments)
    console.log(Object.prototype.toString.call(arguments))
    console.log(Array.isArray(arguments))
}

test('John', 24, 'man')

我们来看一下上面这段代码在Chrome调试台中的打印截图 image.png 从实践中来看,typeof同样无法直接识别类数组,但通过Object.prototype.toString.call返回结果'[object arguments]',而不等于数组的'[object array]',同时,ES6的isArray返回也是false,说明类数组是有别于数组的。

另外我们从打印中可以看到,除了length属性和Iterator接口之外,它还有一个callee的属性,我们把它打印出来看看

function test(name, age, sex) {
    console.log(arguments.callee)
}
test('John', 24, 'man')

image.png

打印出来的即是函数本身,也叫做函数的引用,只在函数运行时有效。
它的作用大致有那么几个:

  1. 它有一个属性length,代表形参的个数,可以通过arguments.length === arguments.callee.length来判断形参和实参的个数是否相等;
  2. 降低一小部分递归代码的耦合度,callee不会因为函数名字的更改而更改引用地址,用代码来解释如下:
    // 比如阶乘,!5 = 5 * 4 * 3 * 2 * 1
    var factorial = function(num) {
        // 如果用注释掉的这行代码那么当factorial被赋值为null时,该行会报错,如果使用callee就不用跟随外部函数名去更改内部的这个函数名字
        // return num * (num <= 1 ? 1 : factorial(num - 1))
        return num * (num <= 1 ? 1 : arguments.callee(num - 1))
    }
    
    var jiecheng = factorial
    factorial = null
    jiecheng(5)
    

HTMLCollection

HTMLCollection是HTML DOM对象(document)的一个接口,这个接口包含了获取到的 DOM 元素集合,返回的类型是类数组对象,如果用 typeof 来判断的话,它返回的是 'object'。它是跟所有引用类型数据一样是即时更新的,即打印出来的状态会是。

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

image.png 其实不一定只有children,你可以使用console.dir(document)会发现里面有很多的HTMLCollection,如有表单的页面你可以打印出[object HTMLFormElement]。

NodeList

细心的大佬们打印document的时候发现,在document.children上面一行(正在是刚好上一行)有一个document.childNodes,它的结构是NodeList。

image.png 它是一个节点的集合,也是一个类数组,通常由document.querySelector(),我们最常见到的情况checkbox,如var list = document.querySelectorAll('input[type=checkbox]')

类数组有什么用?

上面讲了那么多类数组的结构,那么我们究竟探究类数组有什么用?在工作开发中能给我们带来一些什么样的解决方案?

不定参数操作

如今ES6盛行的当下我们要做不定参数操作可以使用到三点运算符(扩展运算符),但是在ES5或者需要自己手写兼容的时候,那么arguments的作用就来了,比如加法函数:

// ES5
function add() {
    var i = 0
    var sum = 0
    while(i < arguments.length) {
        sum += arguments[i]
        i++
    }
    return sum
}

// ES6
function add(...rest) {
    let i = 0
    let sum = 0
    while(i < rest.length) {
        sum += rest[i]
        i++
    }
    return sum
}

add() // 0
add(1) // 1
add(1,2,3,4) // 10

又比如需要透传参数

function foo() {
    bar.apply(this, arguments)
}
function bar(a, b, c) {
   console.log(a, b, c)
}
foo(1, 2, 3)   // 1 2 3

类数组怎么转变成数组

我们在面试中其实经常会遇到这个面试题,这里有两种思路,一种是让类数组真正转变为数组,另一种是让它更像数组(可以调用数组的方法),就拿前面的加法函数来举例:

转变为真数组

  • Array.from
function sum() {
  const args = Array.from(arguments)
  return args.reduce((sum, current) => sum + current))
}
console.log(sum(1, 2))    // 3

function sum(...args) {
  console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2);    // 3

  • 扩展运算符
function sum(a, b) {
  const args = [...arguments]
  return args.reduce((sum, current) => sum + current)
}
console.log(sum(1, 2))    // 3

通过call、bind、apply等借用数组方法

const alinkArr = { 
  0: 1,
  1: 2,
  length: 2
} 
console.log(Array.prototype.reduce.call(alinkArr, (sum, current) => sum + current))
// 3

总结一下

在前端工作中,由于不太常见或者应用不广泛,我们会对类数组不屑一顾,但是在大厂编程工作中经常需要将类数组向数组转化,尤其是一些技术类的内部开源项目,经常会看到函数中处理参数的写法,例如:[].slice.call(arguments) 这行代码。

本文到此告一段段落,希望自己日后能更加细心学习,越走越远。