jimojianghu

前面博文有介绍JavaScript中数组的一些特性,通过对这些数组特性的深入梳理,能够加深我们对数组相关知识的理解,详见博文:
一文搞懂JavaScript数组的特性

其实,在前端开发中,除了数组以外,还有一种类似数组的对象,一般叫做类数组、或伪数组,也是我们需要掌握的知识点。

类数组是什么?

首先,我们先尝试给类数组加个简单的定义:拥有length属性的对象(非数组)。类数组的核心特征就是拥有length属性,拥有length属性又不是真正数组的对象,基本可以被认定为类数组。
虽然这个定义很简单,只突出了length属性,但类数组的基本特点,我们还是可以总结如下:

  • 拥有length属性
  • length属性非动态值,不会随元素变化而变化
  • 可以使用数字索引下标访问属性元素
  • 不是数组,没有数组对应的各种方法
  • 可以使用for语句等进行循环遍历
  • 能够通过一定方式转换成真正的数组

创建类数组的对象

根据以上类数组的特点,我们可以自己创建类数组的对象,也比较简单,只需要拥有length,如下所示:

const al = { 0: 111, 1: 222, 2: 333, length: 3 }
for (let i = 0; i < al.length; i++) {
  console.log(al[i])
}
// 111
// 222
// 333

以上代码,创建了一个length属性为3的对象,通过for循环遍历对象,并且下标输出对应的值。
length属性并不会动态添加,比如我们给al对象增加一个属性,但length值仍为3:

al[3] = 444
console.log(al.length) // 3

如果把该类数组对象转换成一个真正的数组后,就可以进行数组操作了。

事实上,我们可以省略其他属性,只保留length,同样可当做一个类数组使用:

const al = { length: 3 }

虽然我们可以通过类似这种方式,自己创建类数组,但实际开发过程中,几乎没人这么处理。
真正在前端开发中,接触到的类数组,都是JavaScript语言或者Web环境下提供的各种类数组的对象。
在介绍这些类数组对象之前,我们先看下类数组对象如何才能转换成数组。

如何将类数组转换成数组

类数组只是类似数组的对象,缺少很多相应的方法,有时候我们可能需要把类数组转换成数组,才能更好的操作,这时候就需要找到方便有效的转换方式。
当前常用的转换方式,大致有如下几类,我们一个个介绍。

Array.from()

首先是ES6提供的新的数组静态方法:Array.from,它用于从一个类数组或可迭代对象中创建一个新的数组。基本上,只要拥有length属性的对象,都能被Array.from转换成数组。
语法:Array.from(arrayLike, mapFn, thisArg)
三个参数说明:

  • arrayLike:类数组对象,或可迭代对象
  • mapFn:可选参数,新数组会经过该函数处理后返回
  • thisArg:可选参数,执行mapFn时指定的this指向

下面看一个示例:

const al = { 0: 111, 1: 222, 2: 333, length: 3 }
const arr = Array.from(al)
console.log(arr) // [111, 222, 333]
console.log(arr.length) // 3
console.log(Array.isArray(arr)) // true

以上代码,我们自定义了一个拥有length属性的对象,然后通过Array.from方法进行转换,得到了一个长度为3的新数组。
这种方式简单方便,在当前前端开发的绝大多数情况下,都应该是首选。

Array.prototype.slice.call()

在ES6没出来之前,如果想较好的进行类数组的转换,一般使用的是该方法:Array.prototype.slice.call()。
它基于数组的slice()方法,把执行时的this指向类数组对象,让类数组对象像数组一样使用slice()方法返回一个新的数组,因此拥有length属性的类数组对象都能被转换成数组。

const al = { 0: 111, 1: 222, 2: 333, length: 3 }
const arr = Array.prototype.slice.call(al)
console.log(arr) // [111, 222, 333]

以上代码,与Array.from定义同样的类数组对象,通过Array.prototype.slice.call转换,效果是一样的。

其他方式

以上两种方式,是最正确有效的转换,推荐日常使用。
除此以外,还有其他的方式,但各有缺陷,要么较复杂、要么对部分类数组对象不适用,所以并不太推荐。

  • for循环处理
    定义一个新的空数组,通过循环遍历类数组对象的属性,将类数组对象的每一个属性元素值都添加到新数组中。
    这种方式就显得相对麻烦一些。
  • 扩展运算符(...)
    扩展运算符主要作用于可迭代对象,JavaScript中的集合(数组、Set、Map)都是可迭代对象,另外字符串和arguments也都是。
    扩展运算符对于不可迭代对象,会报错。
[...'hello'] // ['h', 'e', 'l', 'l', 'o']
[...{ length: 3 }] // Uncaught TypeError: {(intermediate value)} is not iterable
Array.from({ length: 3 }) // [undefined, undefined, undefined]

以上代码,通过扩展运算符,字符串能被转换成数组,但自定义类数组对象 { length: 3 } 则不行,并报错;而使用Array.from则转成了拥有3个 undefined 元素的数组。

类数组对象

前端开发中最常见且被认可的类数组对象,是函数的arguments参数对象,另外字符串也是一个类数组对象,其他的还有各种Web环境提供的API。
如果把这些分成两类的话:

  • JS对象
    属于JavaScript语言的对象,在nodejs中也存在。
    如arguments、字符串、TypeArray、自定义类数组等。
  • Web对象
    依赖于Web环境(一般是浏览器)的对象,不属于JavaScript语言。
    如FileList、DOM列表对象、数据存储(如localStorage、sessionStorage)等。

下面我们就一一介绍下这些类数组对象。

JavaScript类数组对象

arguments

arguments是JS中函数内部的一个对象,用于处理不定数目的参数,是一个类数组对象。
在ES6推出之前,arguments的使用还是非常广泛的,但在ES6推出默认参数和扩展参数后,它的使用就减少了。

通过下面的示例,转成数组:

function test () {
  console.log(Array.prototype.toString.call(arguments))
  console.log(Array.from(arguments))
  console.log([...arguments], Array.prototype.slice.call(arguments))
}
test(23)
// [object Arguments]
// [23]
// [23] [23]

以上代码可知,该对象存在独特的类型判断 Arguments,可用扩展运算符处理arguments对象。

注意,箭头函数内部不存在arguments对象。

字符串

JavaScript中的字符串也是一个类数组对象,它拥有length属性,可以通过下标数字索引访问,也可以转换成数组,字符串中的每一个字符都变成新数组的一个元素。

const str = 'abc'
str.length // 3
str[1] // 'b'
Array.from(str) // ['a', 'b', 'c']
[...str] // ['a', 'b', 'c']

字符串拥有一个实例方法 split,可以将字符串按照传入的分隔符进行处理,返回一个分割出来的每个子字符串都作为元素的数组:

'abc'.split('') // ['a', 'b', 'c']
'abc'.split('b') // ['a', 'c']

以上代码,
当使用空字符做分隔符的时候,就是将每一个字符都分割成数组元素,与类数组的转换方式相同。
当使用 b 字符做分隔符的时候,返回的数组元素就只有2个了。

由于split操作分隔符的灵活性,可以给我们处理字符串带来方便,所以常用该方法,而少用类数组转换。

TypeArray类型数组

类型数组也是一种类数组对象,提供了访问内存缓冲区中二进制数据的机制,它拥有11个对象,如Uint8Array、Uint8ClampedArray、Int32Array、Float64Array等等。具体知识可见博文前端二进制API知识总结

类型数组都拥有和数组相同的大部分用于遍历的实例方法,但它不是真正的数组,它不能动态变化,不支持push、pop、shift、unshift等修改数组的方法,类型判断也不是数组:

const u8 = new Uint8Array()
u8 instanceof Uint8Array // true
Array.isArray(u8) // false
[...u8] // []
Array.from(u8) // []

以上代码,可以看出,类型数组并不是真正的数组,但可以通过扩展运算符转换成数组(其他方式也行)。
当然,类型数组的使用场景一般在于处理二进制数据,能够遍历读取数据做一些操作即可,并不太需要做转换。

其他JS类数组对象

  • 自定义类数组对象
    前面已有介绍,开发中几乎不用。

  • function
    函数作为一个对象,也拥有length属性,所以它也可以转换成数组。
    函数的length属性表示的是参数的个数,转成数组后,就表示有几个元素,但元素值都为undefined。

    const fun = (al) => {}
    fun.length // 1
    Array.from(fun) // [undefined]
    

Web-API类数组对象

FileList

FileList对象是一个我们常常用到的类数组对象,主要是在文件上传的过程中,我们选择文件后,前端用于接收文件信息的对象,就是它,可以读取到单个或多个文件数据。

FileList是一个拥有length属性并且属性索引值是数字的对象,它的每一个子成员属性都是一个 File 对象,处理文件信息。

inputFile.addEventListener('change', (e) => {
  const files = e.target.files
  console.log(Array.from(files))
})
// [File]

以上代码,就是监听一个上传控件的change事件,读取到对应的文件列表,并转换输出为数组,元素为File对象。

一般也用不着转换,直接遍历读取FileList即可。

DOM中类数组

在Web开发中,DOM操作也有很多类数组对象,如元素集合HTMLCollection、节点NodeList等等。

HTMLCollection

HTMLCollection来自于页面的document等节点元素对象,主要是各种属性:

  • document.links:返回所有链接元素
  • docuement.forms:返回所有表单元素
  • document.images:返回所有图像元素
  • document.scripts:返回所有脚本元素
  • document.embeds:返回所有embed嵌入对象
  • Element.children:返回当前节点的所有子元素
const links = document.links
Object.prototype.toString.call(links) // '[object HTMLCollection]'
const linkArr = Array.from(links) // []
Array.isArray(linkArr) // true

以上代码,读取了页面所有的链接,返回的一个HTMLCollection类数组对象,可以转换成对应的数组,当前页面并不存在链接,所以返回的是空数组。

NodeList

通过以下方法或属性获取的节点列表(NodeList):

  • getElementsByTagName:返回所有拥有指定HTML标签名的节点
  • getElementsByClassName:返回所有拥有指定class类属性名的节点
  • getElementsByName:返回所有拥有指定name属性的节点
  • querySelectorAll:返回所有匹配给定选择器的节点
  • document.childNodes:返回所有子节点
const divList = document.querySelectorAll('div')
Array.isArray(divList) // false
Object.prototype.toString.call(divList) // '[object NodeList]'
divList.length // 4
const divArr = [...divList] //  [div, div#clickInput, div#name, div#dropArea]
Array.isArray(divArr) // true

以上代码,通过 querySelectorAll 读取了页面上所有的div,得到了一个NodeList对象,是一个类数组对象,可以通过扩展运算符转换成一个真正的数组。

其他DOM相关类数组对象

以下也是DOM操作中,常见的一些类数组对象:

  • document.styleSheets:返回所有样式表(StyleSheetList)
  • attributes:返回节点元素的所有属性(NamedNodeMap)
  • dataset:返回节点元素上的所有附加数据(DOMStringMap)
  • classList:返回节点元素上的所有样式类(DOMTokenList)

DOM中的对象都是由DOM-API提供给JavaScript调用的,因为不止JS要用,所以使用类数组较合适。

其他Web类数组对象

  • window
    window对象也拥有length,所以它也是类数组对象?
    是的,window的length属性表示页面拥有的frame/iframe的数量。
    所以也可以转换成数组:

    window.length // 0
    Array.from(window) // []
    

    当前页面没有frame,长度为0,转换成了空数组。

  • DataTransferItemList
    用于从拖拽或粘贴事件中读取到的 Datatransfer 对象的一个只读属性 items,同一级的只读属性还有 files,也是一个类数组对象 FileList,上面已有介绍。
    在粘贴事件中,可以通过该对象读取到粘贴的数据内容。

  • 数据存储
    浏览器中的数据存储机制,如 sessionStoragelocalStorage,他们其实也是类数组对象,拥有length属性,可以进行转换。

    localStorage.length // 2
    Array.from(localStorage) // [undefined, undefined]
    

    以上代码,就是对localStorage的处理,拥有两个值,但由于是字符串键值对,无法转成数字索引,所以数组两个元素都是 undefined。

总结

本文描述了什么是类数组对象,它的主要特点,以及如何创建一个类数组对象,还有将类数组对象转换成真正数组的几种方式,接着也介绍了前端开发中可能会遇到的已有的各种类数组对象。
事实上,绝大部分的类数组对象是不需要专门去转换为数组的,直接遍历操作即可,但深入掌握这些类数组的知识,也是前端开发中不可少的。

相关文章: