前面博文有介绍JavaScript中数组的一些特性,通过对这些数组特性的深入梳理,能够加深我们对数组相关知识的理解,详见博文:
一文搞懂JavaScript数组的特性
其实,在前端开发中,除了数组以外,还有一种类似数组的对象,一般叫做类数组、或伪数组,也是我们需要掌握的知识点。
首先,我们先尝试给类数组加个简单的定义:拥有length属性的对象(非数组)。类数组的核心特征就是拥有length属性,拥有length属性又不是真正数组的对象,基本可以被认定为类数组。
虽然这个定义很简单,只突出了length属性,但类数组的基本特点,我们还是可以总结如下:
根据以上类数组的特点,我们可以自己创建类数组的对象,也比较简单,只需要拥有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环境下提供的各种类数组的对象。
在介绍这些类数组对象之前,我们先看下类数组对象如何才能转换成数组。
类数组只是类似数组的对象,缺少很多相应的方法,有时候我们可能需要把类数组转换成数组,才能更好的操作,这时候就需要找到方便有效的转换方式。
当前常用的转换方式,大致有如下几类,我们一个个介绍。
首先是ES6提供的新的数组静态方法:Array.from,它用于从一个类数组或可迭代对象中创建一个新的数组。基本上,只要拥有length属性的对象,都能被Array.from转换成数组。
语法:Array.from(arrayLike, mapFn, thisArg)
。
三个参数说明:
下面看一个示例:
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的新数组。
这种方式简单方便,在当前前端开发的绝大多数情况下,都应该是首选。
在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转换,效果是一样的。
以上两种方式,是最正确有效的转换,推荐日常使用。
除此以外,还有其他的方式,但各有缺陷,要么较复杂、要么对部分类数组对象不适用,所以并不太推荐。
[...'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。
如果把这些分成两类的话:
下面我们就一一介绍下这些类数组对象。
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操作分隔符的灵活性,可以给我们处理字符串带来方便,所以常用该方法,而少用类数组转换。
类型数组也是一种类数组对象,提供了访问内存缓冲区中二进制数据的机制,它拥有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) // []
以上代码,可以看出,类型数组并不是真正的数组,但可以通过扩展运算符转换成数组(其他方式也行)。
当然,类型数组的使用场景一般在于处理二进制数据,能够遍历读取数据做一些操作即可,并不太需要做转换。
自定义类数组对象
前面已有介绍,开发中几乎不用。
function
函数作为一个对象,也拥有length属性,所以它也可以转换成数组。
函数的length属性表示的是参数的个数,转成数组后,就表示有几个元素,但元素值都为undefined。
const fun = (al) => {} fun.length // 1 Array.from(fun) // [undefined]
FileList对象是一个我们常常用到的类数组对象,主要是在文件上传的过程中,我们选择文件后,前端用于接收文件信息的对象,就是它,可以读取到单个或多个文件数据。
FileList是一个拥有length属性并且属性索引值是数字的对象,它的每一个子成员属性都是一个 File
对象,处理文件信息。
inputFile.addEventListener('change', (e) => { const files = e.target.files console.log(Array.from(files)) }) // [File]
以上代码,就是监听一个上传控件的change事件,读取到对应的文件列表,并转换输出为数组,元素为File对象。
一般也用不着转换,直接遍历读取FileList即可。
在Web开发中,DOM操作也有很多类数组对象,如元素集合HTMLCollection、节点NodeList等等。
HTMLCollection来自于页面的document等节点元素对象,主要是各种属性:
const links = document.links Object.prototype.toString.call(links) // '[object HTMLCollection]' const linkArr = Array.from(links) // [] Array.isArray(linkArr) // true
以上代码,读取了页面所有的链接,返回的一个HTMLCollection类数组对象,可以转换成对应的数组,当前页面并不存在链接,所以返回的是空数组。
通过以下方法或属性获取的节点列表(NodeList):
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中的对象都是由DOM-API提供给JavaScript调用的,因为不止JS要用,所以使用类数组较合适。
window
window对象也拥有length,所以它也是类数组对象?
是的,window的length属性表示页面拥有的frame/iframe的数量。
所以也可以转换成数组:
window.length // 0 Array.from(window) // []
当前页面没有frame,长度为0,转换成了空数组。
DataTransferItemList
用于从拖拽或粘贴事件中读取到的 Datatransfer
对象的一个只读属性 items
,同一级的只读属性还有 files
,也是一个类数组对象 FileList
,上面已有介绍。
在粘贴事件中,可以通过该对象读取到粘贴的数据内容。
数据存储
浏览器中的数据存储机制,如 sessionStorage
和 localStorage
,他们其实也是类数组对象,拥有length属性,可以进行转换。
localStorage.length // 2 Array.from(localStorage) // [undefined, undefined]
以上代码,就是对localStorage的处理,拥有两个值,但由于是字符串键值对,无法转成数字索引,所以数组两个元素都是 undefined。
本文描述了什么是类数组对象,它的主要特点,以及如何创建一个类数组对象,还有将类数组对象转换成真正数组的几种方式,接着也介绍了前端开发中可能会遇到的已有的各种类数组对象。
事实上,绝大部分的类数组对象是不需要专门去转换为数组的,直接遍历操作即可,但深入掌握这些类数组的知识,也是前端开发中不可少的。