跳转到内容

第八章 数组的扩展

讲完函数扩展后,本章节阮一峰老师从 运算符和实例相关方法 讲解数组的扩展。

扩展运算符

扩展运算符(spread)是在ES6中引入的一个新特性,用三个点(...)表示。它的作用是将一个数组“展开”,即将数组中的每个元素以逗号分隔的形式展开,可以方便地将数组转换为参数序列。

  1. 将数组展开为参数序列:

    js
    console.log(...[1, 2, 3]); // 输出:1 2 3
  2. 在函数调用时使用扩展运算符:

    js
    function push(array, ...items) {
      array.push(...items);
    }
    
    var numbers = [2, 3, 4];
    push(numbers, 5);
    console.log(...numbers); // 输出:2 3 4 5
  3. 结合表达式使用扩展运算符:

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

若展开的数组是空数组,则没有任何影响。

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

此外,在 ES5 由于有一些场景需要数组使用其他方法,而那些方法数组无法使用,只能通过 apply 转参数。而扩展运算符能够替代数组的 apply 方法:

js
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);
js
var arr3 = [0, 1, 2, ...arr2];

使用扩展运算符简化Math.max方法的应用:

js
Math.max.apply(null, [14, 3, 77]);
js
Math.max(...[14, 3, 77]);

更简洁地创建 Date 对象:

js
new (Date.bind.apply(Date, [null, 2015, 1, 1]));
js
new Date(...[2015, 1, 1]);

扩展运算符可以运用于以下的场景:

  1. 合并数组

    在 ES5 合并数组需要用到 concat 方法,有了扩展运算符后无需其他操作。

    js
    let arr1 = [1, 2]
    let arr2 = [3, 4]
    
    // es5
    arr1.concat(arr2) // [1, 2, 3, 4]
    
    // es6
    [...arr1, ...arr2] // [1, 2, 3, 4]
  2. 结合解构赋值

    结合前面讲过的解构赋值,可以快速生成数组。但是要注意扩展运算符需要放到参数的最后一位。

    js
    let [a, ...b] = [1, 2, 3, 4]
    console.log(a, b) // 1, [2, 3, 4]
    
    let [x, y, ...z] = [1]
    console.log(x, y, z) // 1, undefined, []
    
    let [...i, j] = [] // 报错
  3. 函数返回值

    函数 return 只能返回一个值,若返回多个需要使用数组或者对象包裹。扩展运算符可以解决这个问题。

    js
    function fn () {
      return [1, 2]
    }
    
    let [a, b] = fn()
    console.log(a, b) // 1, 2
  4. 字符串

    扩展运算符可以把字符串转为数组,该写法的好处是可以正确识别 32 位的 Unicode 字符。

    js
    console.log([...'hello']) // ['h', 'e', 'l', 'l', 'o']
    
    [...'x\uD83D\uDE80y'].length // 3
  5. Iterator 接口对象

    任何有 Iterator 接口对象都可以使用扩展运算符,并转为真正的数组。

    js
    [...document.querySelectorAll('div')]
    
    let map = new Map([
      [1, 'a'],
      [2, 'b'],
    ])
    [...map.keys()] // [1, 2]

Array.from()

Array.from 方法用于将类似数组的对象和可遍历对象转换为真正的数组。

在实际应用中,常见的类似数组的对象包括 DOM 操作返回的 NodeList 集合以及函数内部的 arguments 对象。Array.from 可以将这些对象转为真正的数组,使其可以使用数组方法进行操作。

js
// 处理类似数组的对象和可遍历对象
let ps = document.querySelectorAll('p');
Array.from(ps).forEach(function(p) {
  console.log(p);
});

// 将类似数组的对象转为数组
let arrayLike = {
  '0': 'a',
  '1': 'b',
  '2': 'c',
  length: 3
};
let arr1 = Array.from(arrayLike);
console.log(arr1); // ['a', 'b', 'c']

function fn () {
  var argm = Array.from(arguments)
}

除了类似数组的对象,部署了 Iterator 接口的数据结构也可以通过 Array.from 转为数组,如字符串和 Set 结构。如果参数本身就是一个数组,Array.from 会返回一个新的相同的数组。

扩展运算符(...)也可以将某些数据结构转为数组,但只能转换部署了遍历器接口的对象。Array.from 本质上可以转换所有含 lenght 属性的对象,而扩展运算符无法转换这种对象。

Array.from 还可以接受第二个参数,类似于数组的 map 方法,用来对每个元素进行处理。这样可以在转换为数组的同时对每个元素进行操作,非常灵活。

js
// 使用 map 函数处理每个元素
let arr3 = Array.from([1, 2, 3], x => x * x);
console.log(arr3); // [1, 4, 9]

// 绑定 this 关键字
let obj = {
  length: 3,
  createArray: function() {
    return Array.from({ length: this.length }, () => 'jack');
  }
};
let arr4 = obj.createArray();
console.log(arr4); // ['jack', 'jack', 'jack']

// 将字符串转为数组,并返回字符串的长度
function countSymbols(string) {
  return Array.from(string).length;
}
console.log(countSymbols('hello')); // 5

Array.of()

Array.of 方法是一个用于将一组值转换为数组的方法。它的主要目的是弥补了使用 Array 构造函数时参数个数不同导致行为差异的问题。

  1. Array.of 方法基本上可以用来替代 Array()new Array(),并且不存在由于参数不同而导致的重载,它的行为非常统一。
  2. Array.of 方法会始终返回由参数值组成的数组。如果没有参数,就返回一个空数组。
  3. 当使用 Array() 构造函数时,参数个数不同会导致不同的行为,而使用 Array.of 方法可以解决这一问题。
js
// 使用 Array.of 方法创建数组
let arr1 = Array.of(3, 11, 8);
console.log(arr1); // [3, 11, 8]

let arr2 = Array.of(3);
console.log(arr2); // [3]

console.log(Array.of(3).length); // 1

// 使用模拟实现的 ArrayOf 函数
function ArrayOf() {
  return Array.prototype.slice.call(arguments);
}

let arr3 = ArrayOf(undefined);
console.log(arr3); // [undefined]

let arr4 = ArrayOf(1, 2);
console.log(arr4); // [1, 2]

总而言之,该方法能够统一不同参数个数下创建数组的行为,避免了因参数不同而导致的问题。

copyWithin()

数组实例的 copyWithin 方法用于在当前数组内部将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。这意味着使用这个方法会修改当前数组。具体来说,该方法接受三个参数:

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

这些参数都应该是数值,如果不是,会自动转为数值。

js
// 示例 1
// 将从第3号位置直到数组结束的成员(3, 4, 5)复制到从第0号位置开始的位置,结果覆盖了原来的成员
[1, 2, 3, 4, 5].copyWithin(0, 3);
// 结果为 [4, 5, 3, 4, 5]

// 示例 2
// 将第3号位复制到第0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4);
// 结果为 [4, 2, 3, 4, 5]

// 示例 3
// -2 相当于倒数第二位,-1 相当于倒数第一位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1);
// 结果为 [4, 2, 3, 4, 5]

// 示例 4
// 将第3号位到数组结束复制到第0号位
var i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// 结果为 Int32Array [3, 4, 5, 4, 5]

// 示例 5
// 对于没有部署 TypedArray copyWithin 方法的平台,需要采用下面的写法
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// 结果为 Int32Array [4, 2, 3, 4, 5]

总而言之,这个方法可以方便地在数组内部进行成员的复制和替换操作。

find()和findIndex()

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

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

js
// 示例 1
// 找出数组中第一个小于0的成员
[-1, 4, -5, 10].find((n) => n < 0);
// 结果为 -1

// 示例 2
// 找出数组中第一个大于9的成员
[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
});
// 结果为 10

// 示例 3
// 找出数组中第一个满足条件的成员的位置
[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
});
// 结果为 2

// 示例 4
// 使用箭头函数作为回调函数,并绑定this对象
[1, 5, 10, 15].find((value, index, arr) => {
  return value > this.threshold;
}, { threshold: 9 });
// 结果为 10

总而言之,这两个方法非常适合用于查找数组中满足特定条件的成员或者获取符合条件成员的位置。此外还可以识别 NaN ,弥补了 indexOf 方法无法识别 NaN 的不足。

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

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

fill()

数组的 fill() 方法用于给数组填充一个定值,会覆盖原数组的值。接收三个参数:

  • target(必传):要被填充的定值
  • start(选传):填充的起始位置
  • end(选传):填充的结束位置
js
// 给空数组填充定值
new Array(3).fill(7) // [7, 7, 7]

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

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

Entries()、keys()、values()

ES6提供了三个新的数组遍历方法:entries()keys()values() 。这些方法都返回一个遍历器对象,可以通过for...of循环进行遍历。它们之间的区别在于:

  • keys() 方法用于对数组的键名(索引)进行遍历。
  • values() 方法用于对数组的键值进行遍历。
  • entries() 方法用于对数组的键值对进行遍历。

以下是一些使用这些方法的示例及其说明:

js
// 示例 1
// 使用keys()方法遍历数组的键名
for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 输出:
// 0
// 1

// 示例 2
// 使用values()方法遍历数组的键值
for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 输出:
// 'a'
// 'b'

// 示例 3
// 使用entries()方法遍历数组的键值对
for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 输出:
// 0 'a'
// 1 'b'

// 手动调用遍历器对象的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()

ES2016 引入了 Array.prototype.includes 方法,用于判断数组是否包含给定的值,类似于字符串的 includes 方法。

js
[1, 2, 3].includes(2) // true 数组中包含值为2
[1, 2, 3].includes(4) // false 数组中不包含值为4
[1, 2, NaN].includes(NaN) // true 数组中包含NaN

该方法还可以接受第二个参数,表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示从末尾开始的位置。如果起始位置大于数组长度,则会从头开始搜索。

js
[1, 2, 3].includes(3, 3) // false 从索引3开始搜索,数组中没有找到值为3
[1, 2, 3].includes(3, -1) // true 从倒数第一个位置开始搜索,数组中找到了值为3

与使用 indexOf 方法相比,includes 方法更加语义化,并且避免了 NaN 的误判问题。

下面是用于检查当前环境是否支持 Array.prototype.includes 方法的代码,并提供一个简易的替代版本:

js
const contains = (() => 
  Array.prototype.includes
    ? (arr, value) => arr.includes(value)
    : (arr, value) => arr.some(el => el === value)
)();

contains(['foo', 'bar'], 'baz'); // 返回 false

需要注意的是,Map 数据结构有 has() 方法来查找键名,而 Set 数据结构有 has() 方法来查找值。区分使用时要注意这两者的不同。

总而言之,Array.prototype.includes 方法提供了一种简便的方式来检查数组中是否包含特定值,避免了 indexOf 方法的一些缺点,并且具有更好的语义性。

数组的空位

空位是指数组中某个位置没有任何值,而不是简单的 undefined 。这在 ES5 中处理起来非常不一致,在大多数情况下会忽略空位。比如,forEachfiltereverysome 方法都会跳过空位,而 map 方法会跳过空位但会保留这个值。而 jointoString 方法会将空位视为 undefined

在 ES6 中,空位被明确转换为 undefinedArray.from 方法会将数组的空位转换为 undefined ,并且不会忽略空位。扩展运算符(...)也会将空位转换为 undefinedcopyWithin 方法会连同空位一起复制,fill方法会将空位视为正常的数组位置。另外,for...of 循环也会遍历空位,而 entrieskeysvaluesfindfindIndex 等方法会将空位处理成 undefined

总的来说,由于空位的处理规则非常不统一,建议尽量避免出现空位。

js
javascript复制代码// forEach 方法
[ , 'a' ].forEach((value, index) => console.log(index)); // 输出:1

// filter 方法
['a', , 'b'].filter(x => true); // 返回:['a', 'b']

// every 方法
[ , 'a' ].every(x => x === 'a'); // 返回:true

// some 方法
[ , 'a' ].some(x => x !== 'a'); // 返回:false

// map 方法
[ , 'a' ].map(x => 1); // 返回:[undefined, 1]

// join 方法
[ , 'a', undefined, null].join('#'); // 返回:"#a##"

// toString 方法
[ , 'a', undefined, null].toString(); // 返回:",a,,"

总结

本章节主要从数组的扩展角度出发,详细讲述了数组扩展的方法。在 ES6 中,数组的扩展引入了许多新特性和方法,使得对数组的操作更加方便和灵活。其中,一些常用的方法包括:

  1. 扩展运算符(spread):可以将一个数组展开成逗号分隔的参数序列。
  2. Array.from():将类数组对象或可迭代对象转换为数组。
  3. Array.of():根据传入的参数创建一个新数组。
  4. copyWithin():将数组中指定位置的元素复制到其他位置。
  5. find():返回数组中满足测试函数的第一个元素。
  6. findIndex():返回数组中满足测试函数的第一个元素的索引。
  7. fill():用静态值填充数组的所有元素。
  8. Entries()keys()values():分别返回数组的键值对、键名和值的迭代器。
  9. includes():判断数组是否包含某个特定元素。