数组的扩展
【数组的扩展】
扩展运算符
含义
扩展运算符(spread)是三个点(...
)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
1 | console.log(...[1, 2, 3]) |
该运算符主要用于函数调用。
1 | function push(array, ...items) { |
上面代码中,array.push(...items)
和add(...numbers)
这两行,都是函数的调用,它们都使用了扩展运算符。该运算符将一个数组,变为参数序列。
扩展运算符与正常的函数参数可以结合使用,非常灵活。
1 | function f(v, w, x, y, z) { } |
扩展运算符后面还可以放置表达式。
1 | const arr = [ |
如果扩展运算符后面是一个空数组,则不产生任何效果。
1 | [...[], 1] |
注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。
1 | (...[1, 2]) |
上面三种情况,扩展运算符都放在圆括号里面,但是前两种情况会报错,因为扩展运算符所在的括号不是函数调用。
替代函数的 apply() 方法
由于扩展运算符可以展开数组,所以不再需要apply()
方法将数组转为函数的参数了。
1 | // ES5 的写法 |
下面是扩展运算符取代apply()
方法的一个实际的例子,应用Math.max()
方法,简化求出一个数组最大元素的写法。
1 | // ES5 的写法 |
上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用Math.max()
函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用Math.max()
了。
另一个例子是通过push()
函数,将一个数组添加到另一个数组的尾部。
1 | // ES5 的写法 |
上面代码的 ES5 写法中,push()
方法的参数不能是数组,所以只好通过apply()
方法变通使用push()
方法。有了扩展运算符,就可以直接将数组传入push()
方法。
下面是另外一个例子。
1 | // ES5 |
扩展运算符的应用
(1)复制数组
数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。
1 | const a1 = [1, 2]; |
上面代码中,a2
并不是a1
的克隆,而是指向同一份数据的另一个指针。修改a2
,会直接导致a1
的变化。
ES5 只能用变通方法来复制数组。
1 | const a1 = [1, 2]; |
上面代码中,a1
会返回原数组的克隆,再修改a2
就不会对a1
产生影响。
扩展运算符提供了复制数组的简便写法。
1 | const a1 = [1, 2]; |
上面的两种写法,a2
都是a1
的克隆。
(2)合并数组
扩展运算符提供了数组合并的新写法。
1 | const arr1 = ['a', 'b']; |
不过,这两种方法都是浅拷贝,使用的时候需要注意。
1 | const a1 = [{ foo: 1 }]; |
上面代码中,a3
和a4
是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组。
(4)字符串转为数组
扩展运算符还可以将字符串转为真正的数组。
1 | console.log(...'alex'); // a l e x |
(5)类数组转为数组
1 | // arguments |
Array.from()
前言
在前端开发中经常会遇到类数组,但是我们不能直接使用数组的方法,需要先把类数组转化为数组。本节介绍 ES6 数组的新增方法 Array.from()
,该方法用于将类数组对象(array-like)和可遍历的对象(iterable)转换为真正的数组进行使用。
方法详情
基本语法
Array.from()
方法会接收一个类数组对象然后返回一个真正的数组实例,返回的数组可以调用数组的所有方法。
语法使用:
1 | Array.from(arrayLike[, mapFn[, thisArg]]) |
参数解释:
参数 | 描述 |
---|---|
arrayLike | 想要转换成数组的类数组对象或可迭代对象 |
mapFn | 如果指定了该参数,新数组中的每个元素会执行该回调函数 |
thisArg | 可选参数,执行回调函数 mapFn 时 this 对象 |
类数组转化
所谓类数组对象,就是指可以通过索引属性访问元素,并且对象拥有 length 属性,类数组对象一般是以下这样的结构:
1 | var arrLike = { |
在 ES5 中没有对应的方法将类数组转化为数组,但是可以借助 call 和 apply 来实现:
1 | var arr = [].slice.call(arrLike); |
有了 ES6 的 Array.from()
就更简单了,对类数组对象直接操作,即可得到数组。
1 | var arr = Array.from(arrLike); |
第二个参数 —— 回调函数
在 Array.from
中第二个参数是一个类似 map 函数的回调函数,该回调函数会依次接收数组中的每一项作为传入的参数,然后对传入值进行处理,最得到一个新的数组。
Array.from(obj, mapFn, thisArg)
也可以用 map 改写成这样 Array.from(obj).map(mapFn, thisArg)
。
1 | var arr = Array.from([1, 2, 3], function (x) { |
上面的例子展示了,Array.from
的参数可以使用 map
方法来进行替换,它们是等价的操作。
第三个参数 ——this
Array.from
中第三个参数可以对回调函数中 this 的指向进行绑定,该参数是非常有用的,我们可以将被处理的数据和处理对象分离,将各种不同的处理数据的方法封装到不同的的对象中去,处理方法采用相同的名字。
在调用 Array.from 对数据对象进行转换时,可以将不同的处理对象按实际情况进行注入,以得到不同的结果,适合解耦。
1 | let obj = { |
定义一个 obj
对象可以认作是,Array.from
回调函数中处理数据的方法集合,handle
是其中的一个方法,把 obj
作为第三个参数传给 Array.from
这样在回调函数中可以通过 this
来拿到 obj
对象。
从字符串里生成数组
Array.from()
在传入字符串时,会把字符串的每一项都拆成单个的字符串作为数组中的一项。
1 | Array.from('imooc'); |
从 Set 中生成数组
用 Set
定义的数组对象,可以使用 Array.from()
得到一个正常的数组。
1 | const set = new Set(['a', 'b', 'c', 'd']); |
上面的代码中创建了一个 Set 数据结构,把实例传入 Array.from()
可以得到一个真正的数组。
从 Map 中生成数组
Map
对象保存的是一个个键值对,Map
中的参数是一个数组或是一个可迭代的对象。 Array.from()
可以把 Map 实例转换为一个二维数组。
1 | const map = new Map([[1, 2], [2, 4], [4, 8]]); |
使用案例
创建一个包含从 0 到 99 (n) 的连续整数的数组
- 一般情况下我们可以使用 for 循环来实现。
1 | var arr = []; |
这种方法的主要优点是最直观了,性能也最好的,但是很多时候我们不想使用 for 循环来进行操作。
- 使用 Array 配合 map 来实现。
1 | var arr = Array(100).join(' ').split('').map(function(item,index){return index}); |
Array (100) 创建了一个包含 100 个空位的数组,但是这样创建出来的数组是没法进行迭代的。所以要通过字符串转换,覆盖 undefined,最后调用 map 修改元素值。
- 使用 es6 的
Array.from
实现。
1 | var arr = Array.from({length:100}).map(function(item,index){return index}); |
Array.from({length:100})
可以定义一个可迭代的数组,数组的每一项都是 undefined,这样就非常方便的定义出所需要的数组了,但是这样定义的数组性能最差,具体可以参考 constArray 的测试结果。
数组去重合并
1 | function combine(){ |
首先定义一个去重数组函数,通过 concat 把传入的数组进行合并到一个新的数组中去,通过 new Set () 可以对 arr 进行去重操作,再使用 Array.from()
返回一个拷贝后的数组。
小结
本节讲解了字符串的 Array.from()
方法的使用,用于将类数组对象和可迭代的对象转化真正的数组,在编程中主要用于更加方便的初始化一个有默认值的数组,还可以用于将获取的 html 的 DOM 对象转化为数组,可以使用数组方法进行操作。
Array.of()
Array.of()
方法用于将一组值,转换为数组。
1 | Array.of(3, 11, 8) // [3,11,8] |
这个方法的主要目的,是弥补数组构造函数Array()
的不足。因为参数个数的不同,会导致Array()
的行为有差异。
1 | Array() // [] |
上面代码中,Array()
方法没有参数、一个参数、三个参数时,返回的结果都不一样。只有当参数个数不少于 2 个时,Array()
才会返回由参数组成的新数组。参数只有一个正整数时,实际上是指定数组的长度。
Array.of()
基本上可以用来替代Array()
或new Array()
,并且不存在由于参数不同而导致的重载。它的行为非常统一。
1 | Array.of() // [] |
Array.of()
总是返回参数值组成的数组。如果没有参数,就返回一个空数组。
Array.of()
方法可以用下面的代码模拟实现。
1 | function ArrayOf(){ |
find(),findIndex(),findLast(),findLastIndex()
数组实例的find()
方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true
的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined
。
1 | [1, 4, -5, 10].find((n) => n < 0) |
上面代码找出数组中第一个小于 0 的成员。
1 | [1, 5, 10, 15].find(function(value, index, arr) { |
上面代码中,find()
方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。
数组实例的findIndex()
方法的用法与find()
方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
。
1 | [1, 5, 10, 15].findIndex(function(value, index, arr) { |
这两个方法都可以接受第二个参数,用来绑定回调函数的this
对象。
1 | function f(v){ |
上面的代码中,find()
函数接收了第二个参数person
对象,回调函数中的this
对象指向person
对象。
另外,这两个方法都可以发现NaN
,弥补了数组的indexOf()
方法的不足。
1 | [NaN].indexOf(NaN) |
上面代码中,indexOf()
方法无法识别数组的NaN
成员,但是findIndex()
方法可以借助Object.is()
方法做到。
find()
和findIndex()
都是从数组的0号位,依次向后检查。ES2022 新增了两个方法findLast()
和findLastIndex()
,从数组的最后一个成员开始,依次向前检查,其他都保持不变。
1 | const array = [ |
上面示例中,findLast()
和findLastIndex()
从数组结尾开始,寻找第一个value
属性为奇数的成员。结果,该成员是{ value: 3 }
,位置是2号位。
filter()
filter()
方法用于过滤数组成员,满足条件的成员组成一个新数组返回。
它的参数是一个函数,所有数组成员依次执行该函数,返回结果为true
的成员组成一个新数组返回。该方法不会改变原数组。
1 | [1, 2, 3, 4, 5].filter(function (elem) { |
上面代码将大于3
的数组成员,作为一个新数组返回。
1 | var arr = [0, 1, 'a', false]; |
上面代码中,filter()
方法返回数组arr
里面所有布尔值为true
的成员。
filter()
方法的参数函数可以接受三个参数:当前成员,当前位置和整个数组。
1 | [1, 2, 3, 4, 5].filter(function (elem, index, arr) { |
上面代码返回偶数位置的成员组成的新数组。
filter()
方法还可以接受第二个参数,用来绑定参数函数内部的this
变量。
1 | var obj = { MAX: 3 }; |
上面代码中,过滤器myFilter()
内部有this
变量,它可以被filter()
方法的第二个参数obj
绑定,返回大于3
的成员。
map()
map()
方法将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回。
1 | var numbers = [1, 2, 3]; |
上面代码中,numbers
数组的所有成员依次执行参数函数,运行结果组成一个新数组返回,原数组没有变化。
map()
方法接受一个函数作为参数。该函数调用时,map()
方法向它传入三个参数:当前成员、当前位置和数组本身。
1 | [1, 2, 3].map(function(elem, index, arr) { |
上面代码中,map()
方法的回调函数有三个参数,elem
为当前成员的值,index
为当前成员的位置,arr
为原数组([1, 2, 3]
)。
map()
方法还可以接受第二个参数,用来绑定回调函数内部的this
变量(详见《this 变量》一章)。
1 | var arr = ['a', 'b', 'c']; |
上面代码通过map()
方法的第二个参数,将回调函数内部的this
对象,指向arr
数组。
如果数组有空位,map()
方法的回调函数在这个位置不会执行,会跳过数组的空位。
1 | var f = function (n) { return 'a' }; |
上面代码中,map()
方法不会跳过undefined
和null
,但是会跳过空位。
reduce()
reduce()
方法依次处理数组的每个成员,最终累计为一个值。它们的差别是,reduce()
是从左到右处理(从第一个成员到最后一个成员)。
语法:arr.reduce(function(累计值, 当前元素){}, 起始值)
1 | [1, 2, 3, 4, 5].reduce(function (a, b) { |
上面代码中,reduce()
方法用来求出数组所有成员的和。reduce()
的参数是一个函数,数组每个成员都会依次执行这个函数。如果数组有 n 个成员,这个参数函数就会执行 n - 1 次。
- 第一次执行:
a
是数组的第一个成员1
,b
是数组的第二个成员2
。 - 第二次执行:
a
为上一轮的返回值3
,b
为第三个成员3
。 - 第三次执行:
a
为上一轮的返回值6
,b
为第四个成员4
。 - 第四次执行:
a
为上一轮返回值10
,b
为第五个成员5
。至此所有成员遍历完成,整个方法的返回值就是最后一轮的返回值15
。
reduce()
方法的第一个参数都是一个函数。该函数接受以下四个参数。
- 累积变量。第一次执行时,默认为数组的第一个成员;以后每次执行时,都是上一轮的返回值。
- 当前变量。第一次执行时,默认为数组的第二个成员;以后每次执行时,都是下一个成员。
- 当前位置。一个整数,表示第二个参数(当前变量)的位置,默认为
1
。 - 原数组。
这四个参数之中,只有前两个是必须的,后两个则是可选的。
1 | [1, 2, 3, 4, 5].reduce(function ( |
如果要对累积变量指定初值,可以把它放在reduce()
方法的第二个参数。
1 | [1, 2, 3, 4, 5].reduce(function (a, b) { |
上面代码指定参数a
的初值为10,所以数组从10开始累加,最终结果为25。注意,这时b
是从数组的第一个成员开始遍历,参数函数会执行5次。
建议总是加上第二个参数,这样比较符合直觉,每个数组成员都会依次执行reduce()
方法的参数函数。另外,第二个参数可以防止空数组报错。
1 | function add(prev, cur) { |
上面代码中,由于空数组取不到累积变量的初始值,reduce()
方法会报错。这时,加上第二个参数,就能保证总是会返回一个值。
总结
1 | //reduce 返回函数累计处理的结果,经常用于求和等 |
some(),every()
这两个方法类似“断言”(assert),返回一个布尔值,表示判断数组成员是否符合某种条件。
它们接受一个函数作为参数,所有数组成员依次执行该函数。该函数接受三个参数:当前成员、当前位置和整个数组,然后返回一个布尔值。
some
方法是只要一个成员的返回值是true
,则整个some
方法的返回值就是true
,否则返回false
。
1 | var arr = [1, 2, 3, 4, 5]; |
上面代码中,如果数组arr
有一个成员大于等于3,some
方法就返回true
。
every
方法是所有成员的返回值都是true
,整个every
方法才返回true
,否则返回false
。
1 | var arr = [1, 2, 3, 4, 5]; |
上面代码中,数组arr
并非所有成员大于等于3
,所以返回false
。
注意,对于空数组,some
方法返回false
,every
方法返回true
,回调函数都不会执行。
1 | function isEven(x) { return x % 2 === 0 } |
some
和every
方法还可以接受第二个参数,用来绑定参数函数内部的this
变量。
fill()
arr.fill(value[, start[, end]])
方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。
起始索引,默认值为 0。
终止索引,默认值为 this.length。
1 | ['a', 'b', 'c'].fill(7) |
上面代码表明,fill
方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。
fill
方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
1 | ['a', 'b', 'c'].fill(7, 1, 2) |
上面代码表示,fill
方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。
注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
1 | let arr = new Array(3).fill({name: "Mike"}); |
at()
长久以来,JavaScript 不支持数组的负索引,如果要引用数组的最后一个成员,不能写成arr[-1]
,只能使用arr[arr.length - 1]
。
这是因为方括号运算符[]
在 JavaScript 语言里面,不仅用于数组,还用于对象。对于对象来说,方括号里面就是键名,比如obj[1]
引用的是键名为字符串1
的键,同理obj[-1]
引用的是键名为字符串-1
的键。由于 JavaScript 的数组是特殊的对象,所以方括号里面的负数无法再有其他语义了,也就是说,不可能添加新语法来支持负索引。
为了解决这个问题,ES2022为数组实例增加了at()
方法,接受一个整数作为参数,返回对应位置的成员,并支持负索引。这个方法不仅可用于数组,也可用于字符串和类型数组(TypedArray)。
1 | const arr = [5, 12, 8, 130, 44]; |
如果参数位置超出了数组范围,at()
返回undefined
。
1 | const sentence = 'This is a sample sentence'; |
entries(),keys() 和 values()
ES6 提供三个新的方法——entries()
,keys()
和values()
——用于遍历数组。它们都返回一个遍历器对象,可以用for...of
循环进行遍历,唯一的区别是keys()
是对键名的遍历、values()
是对键值的遍历,entries()
是对键值对的遍历。
1 | for (let index of ['a', 'b'].keys()) { |
includes()
Array.prototype.includes
方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes
方法类似。ES2016 引入了该方法。
1 | [1, 2, 3].includes(2) // true |
该方法的第二个参数表示搜索的起始位置,默认为0
。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4
,但数组长度为3
),则会重置为从0
开始。
1 | [1, 2, 3].includes(3, 3); // false |
没有该方法之前,我们通常使用数组的indexOf
方法,检查是否包含某个值。
1 | if (arr.indexOf(el) !== -1) { |
indexOf
方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1
,表达起来不够直观。二是,它内部使用严格相等运算符(===
)进行判断,这会导致对NaN
的误判。
1 | [NaN].indexOf(NaN) |
includes
使用的是不一样的判断算法,就没有这个问题。
1 | [NaN].includes(NaN) |
另外,Map 和 Set 数据结构有一个has
方法,需要注意与includes
区分。
- Map 结构的
has
方法,是用来查找键名的,比如Map.prototype.has(key)
、WeakMap.prototype.has(key)
、Reflect.has(target, propertyKey)
。 - Set 结构的
has
方法,是用来查找值的,比如Set.prototype.has(value)
、WeakSet.prototype.has(value)
。
toReversed(),toSorted(),toSpliced(),with()
很多数组的传统方法会改变原数组,比如push()
、pop()
、shift()
、unshift()
等等。数组只要调用了这些方法,它的值就变了。现在有一个提案,允许对数组进行操作时,不改变原数组,而返回一个原数组的拷贝。
这样的方法一共有四个。
Array.prototype.toReversed() -> Array
Array.prototype.toSorted(compareFn) -> Array
Array.prototype.toSpliced(start, deleteCount, ...items) -> Array
Array.prototype.with(index, value) -> Array
它们分别对应数组的原有方法。
toReversed()
对应reverse()
,用来颠倒数组成员的位置。toSorted()
对应sort()
,用来对数组成员排序。toSpliced()
对应splice()
,用来在指定位置,删除指定数量的成员,并插入新成员。with(index, value)
对应splice(index, 1, value)
,用来将指定位置的成员替换为新的值。
上面是这四个新方法对应的原有方法,含义和用法完全一样,唯一不同的是不会改变原数组,而是返回原数组操作后的拷贝。
下面是示例。
1 | const sequence = [1, 2, 3]; |
isArray()
前言
在程序中判断数组是很常见的应用,但在 ES5 中没有能严格判断 JS 对象是否为数组,都会存在一定的问题,比较受广大认可的是借助 toString 来进行判断,很显然这样不是很简洁。ES6 提供了 Array.isArray()
方法更加简洁地判断 JS 对象是否为数组。
方法详情
判断 JS 对象,如果值是 Array
,则为 true; 否则为 false。
语法使用:
1 | Array.isArray(obj) |
参数解释:
参数 | 描述 |
---|---|
obj | 需要检测的 JS 对象 |
ES5 中判断数组的方法
通常使用 typeof
来判断变量的数据类型,但是对数组得到不一样的结果
1 | // 基本类型 |
上面的代码中,对于基本类型的判断没有问题,但是判断数组时,返回了 object 显然不能使用 typeof
来作为判断数组的方法。
通过 instanceof 判断
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链。
instanceof
可以用来判断数组是否存在,判断方式如下:
1 | var arr = ['a', 'b', 'c']; |
在解释上面的代码时,先看下数组的原型链指向示意图:
数组实例的原型链指向的是 Array.prototype
属性,instanceof
运算符就是用来检测 Array.prototype
属性是否存在于数组的原型链上,上面代码中的 arr 变量就是一个数组,所有拥有 Array.prototype
属性,返回值 true
,这样就很好的判断数组类型了。
但是,需要注意的是,prototype
属性是可以修改的,所以并不是最初判断为 true
就一定永远为真。
通过 constructor 判断
我们知道,Array 是 JavaScript 内置的构造函数,构造函数属性(prototype)的 constructor
指向构造函数(见下图),那么通过 constructor
属性也可以判断是否为一个数组。
1 | var arr = new Array('a', 'b', 'c'); |
下面我们通过构造函数的示意图来进行分析:
由上面的示意图可以知道,我们 new 出来的实例对象上的原型对象有 constructor
属性指向构造函数 Array,由此我们可以判断一个数组类型。
但是 constructor
是可以被重写,所以不能确保一定是数组,如下示例:
1 | var str = 'abc'; |
上面的代码中,str 显然不是数组,但是可以把 constructor
指向 Array 构造函数,这样再去进行判断就是有问题的了。
constructor
和 instanceof
也存在同样问题,不同执行环境下,constructor
的判断也有可能不正确。
Array.isArray () 的使用
下面我们通过示例来看下 Array.isArray()
是怎样判断数组的。
1 | // 下面的函数调用都返回 true |
上面的代码中对 JavaScript 中的数据类型做验证,可以很好地区分数组类型。
自定义 isArray
在 ES5 中比较通用的方法是使用 Object.prototype.toString
去判断一个值的类型,也是各大主流库的标准。在不支持 ES6 语法的环境下可以使用下面的方法给 Array
上添加 isArray
方法
1 | if (!Array.isArray){ |
小结
本节介绍了判断一个值是数组类型的方法 Array.isArray()
此方法可以很准确地判断数组,学习了在 ES5 中判断数组类型的几个方法的缺陷。在不支持 ES6 的情况下也可以通过 Object.prototype.toString
自定义 Array.isArray()
方法。