# ES6 数组的扩展

# 扩展运算符

扩展运算符(spread)写作三个点(...)。它好比是rest的逆运算,可以把数组转化为用逗号分隔的参数序列。

console.log(...[1, 2, 3]);
// 1, 2, 3

console.log(1, ...[2, 3, 4], 5);
// 1, 2, 3, 4, 5

扩展运算符经常用于函数调用,当函数需要多个参数时,你可以用扩展运算符将数组内的数据作为参数序列传给函数。

function push (arr, item) {
  arr.push(...item);
}

function add (x, y) {
  return x + y;
}

const nums = [3, 6];
add(...nums);
// 9

上面代码中,array.push(...items)add(...numbers)这两行,都是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。

扩展运算符与正常的函数参数可以结合使用,非常灵活。

function f (v, w, x, y, z) {}

const args = [0, 1];
f(-1, ...args, 2, ...[3]);

扩展运算符后面还可以跟表达式。

const arr = {
  ...(x > 0 ? ['a'] : []),
  'b',
};

如果扩展运算符后面是一个空数组,那么它不会产生任何效果。

[...[], 1]
// [1]

警告

如果在一对圆括号内使用扩展运算符,则JavaScrip引擎会认为那是一个函数调用,如果实际上不是,则会报错。

例如:

(...[1, 2])
// Uncaught SyntaxError: Unexpected number

console.log((...[1, 2]))
// Uncaught SyntaxError: Unexpected number

console.log(...[1, 2])
// 1, 2

# apply 和 call

每个函数都包含了两个非继承而来的方法:apply()call(),这两个方法的用途都是在特定的作用域内调用函数,实际上等于设置函数内部this的值。

首先,apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是Array的实例,也可以是arguments对象。例如:

function sum(num1, num2){
    return num1 + num2;
}
function callSum1(num1, num2){
    return sum.apply(this, arguments);// 传入 arguments 对象
}
function callSum2(num1, num2){
    return sum.apply(this, [num1, num2]);// 传入数组
}
console.log(callSum1(10,10));   //20
console.log(callSum2(10,10));   //20

所以,apply()经常用来将数组作为参数去调用需要多个参数的函数。

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

call()在设置作用域上与apply()没有不同,区别在于call()不接收数组作为参数,你只能一个一个传入参数。

function sum(num1, num2){
    return num1 + num2;
}
function callSum(num1, num2){
    return sum.call(this, num1, num2);
}
console.log(callSum(10,10));   //20

# 代替函数的apply方法

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

// ES5 的写法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6的写法
function f(x, y, z) {
  // ...
}
let args = [0, 1, 2];
f(...args);

下面是扩展运算符取代apply方法的一个实际的例子,应用Math.max方法,简化求出一个数组最大元素的写法。

// ES5 的写法
Math.max.apply(null, [14, 3, 77])

// ES6 的写法
Math.max(...[14, 3, 77])

// 等同于
Math.max(14, 3, 77);

上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用Math.max函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用Math.max了。

另一个例子是调用push,将一个数组添加到另一个数组后面。

// ES5的 写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);

// ES6 的写法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);

上面代码的 ES5 写法中,push方法的参数不能是数组,所以只好通过apply方法变通使用push方法。有了扩展运算符,就可以直接将数组传入push方法。

# 主要应用

# 1. 复制数组

数组是复合的数据类型,直接复制的话只能得到指向真正数据的指针,修改其中一个会导致另一个的变化。

ES5 只能通过变通的方法来克隆数组。

const a1 = [1, 2];
const a2 = a1.concat();

a2[0] = 2;
// a[0] = 1

扩展运算符提供了更简便的方法。

const a1 = [1, 2];

// 方法1
const a2 = [...a1];

// 方法2
const [...a2] = a1;

上面的两种写法,a2都是a1的克隆。

# 2. 合并数组

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

const a1 = ['a', 'b'];
const a2 = ['c'];
const a3 = ['d', 'e'];

// ES5 写法
a1.concat(a2, a3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 写法
[...a1, ...a2, ...a3]
// [ 'a', 'b', 'c', 'd', 'e' ]

不过,这两种方法都是浅拷贝。

const a1 = [{foo: 1}];
const a2 = [{bar: 2}];

const a3 = a1.concat(a2);
const a4 = [...a1, ...a2];

a3[0] === a1[0];// true
a4[0] === a1[0];// true

上面代码中,a3和a4是通过不同方法合并的数组,但它们的成员都是对原数组的引用。如果修改了原数组的成员,会同步反映到新数组。

# 3. 与解构赋值结合

扩展运算符还可以和解构赋值结合起来,用于生成数组。

const [first, ...rest] = [1, 2, 3, 4, 5];
first// 1
rest// [2, 3, 4, 5]

const [first, ...rest] = [];
first// undefined
rest// []

const [first, ...rest] = ['foo'];
first// 'foo'
rest// []

注意

如果将扩展运算符用于解构赋值,只能将它放在最后一位,否则会报错。

例如:

const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错

const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错

# 4. 字符串

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

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

上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符。

'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3

上面代码的第一种写法,JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。

function length(str) {
  return [...str].length;
}

length('x\uD83D\uDE80y') // 3

凡是涉及到操作四个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写。

let str = 'x\uD83D\uDE80y';

str.split('').reverse().join('')
// 'y\uDE80\uD83Dx'

[...str].reverse().join('')
// 'y\uD83D\uDE80x'

上面代码中,如果不用扩展运算符,字符串的reverse操作就不正确。

# API拓展

# Array.from()

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

Array.from方法支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。对于还没有部署该方法的浏览器,可以用Array.prototype.slice方法替代。

下面是一个类似数组的对象,Array.from将它转为真正的数组。

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组。

// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {
  return p.textContent.length > 100;
});

// arguments对象
function foo() {
  var args = Array.from(arguments);
  // ...
}

上面代码中,querySelectorAll方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用filter方法。

值得提醒的是,扩展运算符(...)也可以将某些数据结构转为数组。

// arguments对象
function foo() {
  const args = [...arguments];
}

// NodeList对象
[...document.querySelectorAll('div')]

# Array.of()

Array.of()方法,用于将一个参数序列转化为数组。

Array.of(1, 2, 3)// [1, 2, 3]
Array.of()// []
Array.of(3).length// 1

这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。

Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]

上面代码中,Array方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。

Array.of基本上可以用来替代Array()new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。

Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]

Array.of方法可以用下面的代码实现:

function ArrayOf(){
  return [].slice.call(arguments);
}

# arr.find(), arr.findIndex()

数组实例的find方法,用于找到第一个符合条件的数组成员。它接收一个回调函数,数组成员依次执行,直到找到并返回一个返回值为true的成员。如果没有符合条件的成员,则返回undefined

[1, 5, 10, 15].find((value, index, arr) => value > 9) // 10

上面代码中,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。

数组实例的findIndex方法的用法与之对应,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

[1, 5, 10, 15].find((value, index, arr) => value > 9) // 2

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

function f(v){
  return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person);    // 26

上面的代码中,find函数接收了第二个参数person对象,回调函数中的this对象指向person对象。

另外,indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。

[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0

# arr.fill()

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

注意

如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。

例如:

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

let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
arr
// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]

let arr = new Array(3).fill([]);
arr[0].push(5);
arr
// [[5], [5], [5]]

fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。

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

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

# arr.entries(), arr.keys()和arr.values()

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

const arr = ['a', 'b'];

for (let index of arr.keys()) {
  console.log(index);
}
// 0
// 1

for (let value of arr.values()) {
  console.log(value);
}
// 'a'
// 'b'

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

# arr.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开始。

[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true

相比使用indexOf判断数组是否包含某个值,includes的表达更直观,并且不会对NaN误判。

[NaN].indexOf(NaN)
// -1

[NaN].includes(NaN)
// true

# arr.flat()

数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。如果原数组有空位,flat()方法会跳过空位。

[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]

[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]

flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。

[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

[1, 2, [3, [4, 5]]].flat(Infinity)
// [1, 2, 3, 4, 5]