ES6新增特性——数组的扩展

361 阅读11分钟

一、扩展运算符

扩展运算符的详细介绍

1、展开数组:替代函数的 apply() 方法

由于扩展运算符可以展开数组,所以不再需要apply()方法将数组转为函数的参数了。

function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];

// ES5 的写法
f.apply(null, args);

// ES6 的写法
f(...args);

2、复制数组

扩展运算符复制数组属于浅拷贝。

const a1 = [1, 2];

// ES5
const a2 = a1.concat();

//ES6
const a2 = [...a1];   // 写法一
const [...a2] = a1;   // 写法二

3、合并数组

扩展运算符提供了数组合并的新写法。

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合并数组
arr1.concat(arr2, arr3);   // [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 的合并数组
[...arr1, ...arr2, ...arr3]   // [ 'a', 'b', 'c', 'd', 'e' ]

4、与解构赋值结合

将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

const list = [1, 2, 3, 4, 5];

// ES5
a = list[0], b = list.slice(1)   // a=1, b=[2, 3, 4, 5]

// ES6
[a, ...b] = list   // a=1, b=[2, 3, 4, 5]

5、字符串转数组

扩展运算符还可以将字符串转为真正的数组。

[...'hello']
// [ "h", "e", "l", "l", "o" ]

6、实现了 Iterator 接口的对象转数组

任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。

let nodeList = document.querySelectorAll('div');
let array = [...nodeList];

上面代码中,querySelectorAll()方法返回的是一个NodeList对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于NodeList对象实现了 Iterator。

7、Map 和 Set 结构,Generator 函数

扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]

二、Array.from() :类数组转数组

Array.from()方法用于将两类对象转为真正的数组:类似数组的对象和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

常见的类数组有以下三种

  • 字符串
  • 函数的arguments
  • DOM的NodeList

类数组又被成为“伪数组”,因为并不是真正的数组,只是类似于数组而已,类数组具有以下特点:

  • 拥有length属性
  • 可以使用下标方式访问
  • 但不能使用数组的方法

在 ES5 中将类数组转换为真正的数组,可以使用 Array.prototype.slice.apply() 来实现,在 ES6 中,可以使用 Array.from()

// ES5 
const a = "hello";
const arr = Array.prototype.slice.apply(a)   // ["h","e","l","l","e"]

// ES6
const arr = Array.from(a)   // ["h","e","l","l","e"]

三、Array.of() :创建数组

在 ES6 之前创建一个数组有两种方式,一种是使用构造函数 new Array(),另一种是使用数组字面量(即[ ])的方式创建,但是使用构造函数创建数组会产生一些怪异行为,例如:

const arr1 = new Array(); // 输出为[]
const arr2 = new Array(0); // 输出为[]
const arr3 = new Array(1); // 输出为[empty]
const arr4 = new Array(1,2); // 输出为[1,2]

为了解决传统new Array()方式创建的怪异行为,ES6 引入一种新的创建数组的方式,即Array.of()方法,例:

const arr1 = Array.of(); // 输出为[]
const arr2 = Array.of(0); // 输出为[0]
const arr3 = Array.of(1); // 输出为[1]
const arr4 = Array.of(1,2); // 输出为[1,2]

四、copyWithin() :复制元素

数组实例的copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。

Array.prototype.copyWithin(target, start = 0, end = this.length)

// 示例:将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)   // [4, 2, 3, 4, 5]

它接受三个参数(均为数值)。

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。为负值,表示从末尾开始计算。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。为负值,表示从末尾开始计算。

五、find(),findIndex(),findLast(),findLastIndex() :查找元素

1、find()

数组实例的find()方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

let a = [1, 4, -5, 10];

// 找出数组中第一个小于 0 的成员
a.find((n) => n < 0);   // -5

// 回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组
a.find(function(value, index, arr) {
  return value > 9;
})   // 10

2、findIndex()

数组实例的findIndex()方法的用法与find()方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

3、findLast()、findLastIndex()

find()findIndex()都是从数组的0号位,依次向后检查。ES2022 新增了两个方法findLast()findLastIndex(),从数组的最后一个成员开始,依次向前检查,其他都保持不变。

const array = [
  { value: 1 },
  { value: 2 },
  { value: 3 },
  { value: 4 }
];

array.findLast(n => n.value % 2 === 1);    // { value: 3 }
array.findLastIndex(n => n.value % 2 === 1);    // 2

上面示例中,findLast()findLastIndex()从数组结尾开始,寻找第一个value属性为奇数的成员。结果,该成员是{ value: 3 },位置是2号位。


六、fill() :填充数组

fill方法使用给定值,填充一个数组。

['a', 'b', 'c'].fill(7)   // [7, 7, 7]

new Array(3).fill(7)   // [7, 7, 7]

fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

['a', 'b', 'c'].fill(7, 1, 2)   // ['a', 7, 'c']

七、entries(),keys() 和 values() :遍历数组

ES6 提供三个新的方法——entries()keys()values()——用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"

如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。

let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']

八、includes() :判断元素是否存在

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法。

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。


九、flat(),flatMap() :打平数组(数组扁平化)

数组扁平化,即将多维数组转化为一维数组。

1、在 ES5 中,实现数组扁平化的方法有三种:

  • 递归实现
  • toString() 方法
  • join() 方法

2、在 ES6 中,使用 flat() 方法即可实现

语法为 arr.flat(正整数或 Infinity ),正整数表示打平几层,Infinity 表示不管多少层嵌套,都转化为一维数组。

let a = [1, 2, [3, 4], [5, [6, [7, 8]]]]
a.flat(2)   // [1, 2, 3, 4, 5, 6, [7, 8]]
a.flat(Infinity)   // [1, 2, 3, 4, 5, 6, 7, 8]

3、flatMap()

flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

[2, 3, 4].flatMap((x) => [x, x * 2])   // [2, 4, 3, 6, 4, 8]

// 相当于 [[2, 4], [3, 6], [4, 8]].flat()

flatMap()只能展开一层数组。

[1, 2, 3, 4].flatMap(x => [[x * 2]])   // [[2], [4], [6], [8]]

// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()

十、at() :获取索引值元素

长久以来,JavaScript 不支持数组的负索引,如果要引用数组的最后一个成员,不能写成arr[-1],只能使用arr[arr.length - 1]

这是因为方括号运算符[]在 JavaScript 语言里面,不仅用于数组,还用于对象。对于对象来说,方括号里面就是键名,比如obj[1]引用的是键名为字符串1的键,同理obj[-1]引用的是键名为字符串-1的键。由于 JavaScript 的数组是特殊的对象,所以方括号里面的负数无法再有其他语义了,也就是说,不可能添加新语法来支持负索引。

为了解决这个问题,ES2022 为数组实例增加了at()方法,接受一个整数作为参数,返回对应位置的成员,并支持负索引。这个方法不仅可用于数组,也可用于字符串和类数组。

const arr = [5, 12, 8, 130, 44];
arr.at(2) // 8
arr.at(-2) // 130

如果参数位置超出了数组范围,at()返回undefined


十一、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),用来将指定位置的成员替换为新的值。

上面是这四个新方法对应的原有方法,含义和用法完全一样,唯一不同的是不会改变原数组,而是返回原数组操作后的拷贝。


十二、group(),groupToMap() :分组

数组成员分组是一个常见需求,比如 SQL 有GROUP BY子句和函数式编程有 MapReduce 方法。现在为 JavaScript 新增了数组实例方法group()groupToMap(),它们可以根据分组函数的运行结果,将数组成员分组。

1、group()

group()的参数是一个分组函数,原数组的每个成员都会依次执行这个函数,确定自己是哪一个组。

const array = [1, 2, 3, 4, 5];

array.group((num, index, array) => {
  return num % 2 === 0 ? 'even': 'odd';
});
// { odd: [1, 3, 5], even: [2, 4] }

2、groupToMap()

groupToMap()的作用和用法与group()完全一致,唯一的区别是返回值是一个 Map 结构,而不是对象。Map 结构的键名可以是各种值,所以不管分组函数返回什么值,都会直接作为组名(Map 结构的键名),不会强制转为字符串。这对于分组函数返回值是对象的情况,尤其有用。

const array = [1, 2, 3, 4, 5];

const odd  = { odd: true };
const even = { even: true };
array.groupToMap((num, index, array) => {
  return num % 2 === 0 ? even: odd;
});
//  Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }

上面示例返回的是一个 Map 结构,它的键名就是分组函数返回的两个对象oddeven

总之,按照字符串分组就使用group(),按照对象分组就使用groupToMap()


十三、数组的空位

数组的空位指的是,数组的某一个位置没有任何值,比如Array()构造函数返回的数组都是空位。

Array(3) // [, , ,] 返回一个具有 3 个空位的数组

注意,空位不是undefined,某一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false

上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值。

1、ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。

  • forEach()filter()reduce()every() 和some()都会跳过空位。
  • map()会跳过空位,但会保留这个值。
  • join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。

2、ES6 则是明确将空位转为undefined

  • Array.from()、扩展运算符(...)、entries()keys()values()find()findIndex()方法会将数组的空位,转为undefined,也就是说,这些方法不会忽略空位。
  • copyWithin()会连空位一起拷贝。
  • fill()会将空位视为正常的数组位置。
  • for...of循环也会遍历空位。

由于空位的处理规则非常不统一,所以建议避免出现空位。


十四、Array.prototype.sort() 的排序稳定性

排序稳定性(stable sorting)是排序算法的重要属性,指的是排序关键字相同的项目,排序前后的顺序不变。

const arr = [
  'peach',
  'straw',
  'apple',
  'spork'
];

const stableSorting = (s1, s2) => {
  if (s1[0] < s2[0]) return -1;
  return 1;
};

arr.sort(stableSorting)
// ["apple", "peach", "straw", "spork"]