18. 数组的新特性
目录
请支持本书:购买 (PDF, EPUB, MOBI)捐赠
(广告,请不要屏蔽。)

18. 数组的新特性



18.1 概述

新的静态 `Array` 方法

新的 `Array.prototype` 方法

18.2 新的静态 `Array` 方法

对象 `Array` 有一些新的方法。

18.2.1 `Array.from(arrayLike, mapFunc?, thisArg?)`

`Array.from()` 的基本功能是将两种类型的值转换为数组

以下是将类数组对象转换为数组的示例

const arrayLike = { length: 2, 0: 'a', 1: 'b' };

// for-of only works with iterable values
for (const x of arrayLike) { // TypeError
    console.log(x);
}

const arr = Array.from(arrayLike);
for (const x of arr) { // OK, iterable
    console.log(x);
}
// Output:
// a
// b
18.2.1.1 通过 `Array.from()` 进行映射

`Array.from()` 也是使用 `map()` 泛型 的便捷替代方法

const spans = document.querySelectorAll('span.name');

// map(), generically:
const names1 = Array.prototype.map.call(spans, s => s.textContent);

// Array.from():
const names2 = Array.from(spans, s => s.textContent);

在此示例中,`document.querySelectorAll()` 的结果仍然是一个类数组对象,而不是一个数组,这就是我们无法在其上调用 `map()` 的原因。以前,我们为了调用 `forEach()` 而将类数组对象转换为数组。在这里,我们通过泛型方法调用和 `Array.from()` 的双参数版本跳过了该中间步骤。

18.2.1.2 `from()` 在 `Array` 的子类中

`Array.from()` 的另一个用例是将类数组或可迭代值转换为 `Array` 子类的实例。例如,如果您创建了 `Array` 的子类 `MyArray` 并希望将此类对象转换为 `MyArray` 的实例,则只需使用 `MyArray.from()`。之所以可行,是因为在 ECMAScript 6 中,构造函数会相互继承(超构造函数是其子构造函数的原型)。

class MyArray extends Array {
    ···
}
const instanceOfMyArray = MyArray.from(anIterable);

您还可以将此功能与映射结合使用,以获得一个可以控制结果构造函数的映射操作

// from() – determine the result’s constructor via the receiver
// (in this case, MyArray)
const instanceOfMyArray = MyArray.from([1, 2, 3], x => x * x);

// map(): the result is always an instance of Array
const instanceOfArray   = [1, 2, 3].map(x => x * x);

物种模式允许您配置非静态内置方法(例如 `slice()`、`filter()` 和 `map()`)返回的实例。它在“类”一章的“物种模式”一节中进行了说明。

18.2.2 `Array.of(...items)`

`Array.of(item_0, item_1, ···)` 创建一个数组,其元素为 `item_0`、`item_1` 等。

18.2.2.1 `Array.of()` 作为 `Array` 子类的数组字面量

如果要将多个值转换为数组,则应始终使用数组字面量,尤其是在单个值为数字时,`Array` 构造函数无法正常工作的情况下(有关此怪癖的更多信息

> new Array(3, 11, 8)
[ 3, 11, 8 ]
> new Array(3)
[ , ,  ,]
> new Array(3.1)
RangeError: Invalid array length

但是,如何将值转换为 `Array` 的子构造函数的实例呢?这就是 `Array.of()` 发挥作用的地方(请记住,`Array` 的子构造函数会继承 `Array` 的所有方法,包括 `of()`)。

class MyArray extends Array {
    ···
}
console.log(MyArray.of(3, 11, 8) instanceof MyArray); // true
console.log(MyArray.of(3).length === 1); // true

18.3 新的 `Array.prototype` 方法

数组实例可以使用几种新方法。

18.3.1 迭代数组

以下方法有助于迭代数组

上述每种方法的结果都是一个值序列,但它们不是作为数组返回的;它们是通过迭代器逐个显示的。让我们看一个例子。我正在使用 `Array.from()` 将迭代器的内容放入数组中

> Array.from(['a', 'b'].keys())
[ 0, 1 ]
> Array.from(['a', 'b'].values())
[ 'a', 'b' ]
> Array.from(['a', 'b'].entries())
[ [ 0, 'a' ],
  [ 1, 'b' ] ]

我也可以使用展开运算符 (`...`) 将迭代器转换为数组

> [...['a', 'b'].keys()]
[ 0, 1 ]
18.3.1.1 迭代 `[index, element]` 对

您可以将 `entries()` 与 ECMAScript 6 的 `for-of` 循环和解构结合使用,以方便地迭代 `[index, element]` 对

for (const [index, element] of ['a', 'b'].entries()) {
    console.log(index, element);
}

18.3.2 搜索数组元素

Array.prototype.find(predicate, thisArg?)
返回回调函数 `predicate` 返回 `true` 的第一个数组元素。如果没有这样的元素,则返回 `undefined`。例子

> [6, -5, 8].find(x => x < 0)
-5
> [6, 5, 8].find(x => x < 0)
undefined

Array.prototype.findIndex(predicate, thisArg?)
返回回调函数 `predicate` 返回 `true` 的第一个元素的索引。如果没有这样的元素,则返回 `-1`。例子

> [6, -5, 8].findIndex(x => x < 0)
1
> [6, 5, 8].findIndex(x => x < 0)
-1

回调函数 `predicate` 的完整签名是

predicate(element, index, array)
18.3.2.1 通过 `findIndex()` 查找 `NaN`

`Array.prototype.indexOf()` 的一个众所周知的限制 是它无法找到 `NaN`,因为它通过 `===` 搜索元素

> [NaN].indexOf(NaN)
-1

使用 `findIndex()`,您可以使用 `Object.is()`(在关于 OOP 的章节中解释)并且不会遇到此类问题

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

您还可以采用更通用的方法,方法是创建一个辅助函数 `elemIs()`

> function elemIs(x) { return Object.is.bind(Object, x) }
> [NaN].findIndex(elemIs(NaN))
0

18.3.3 `Array.prototype.copyWithin()`

此方法的签名是

Array.prototype.copyWithin(target : number,
    start : number, end = this.length) : This

它将索引在范围 [ `start` , `end` ) 内的元素复制到索引 `target` 和后续索引。如果两个索引范围重叠,则需要注意在覆盖所有源元素之前先复制它们。

例子

> const arr = [0,1,2,3];
> arr.copyWithin(2, 0, 2)
[ 0, 1, 0, 1 ]
> arr
[ 0, 1, 0, 1 ]

18.3.4 `Array.prototype.fill()`

此方法的签名是

Array.prototype.fill(value : any, start=0, end=this.length) : This

它用给定的 `value` 填充数组

> const arr = ['a', 'b', 'c'];
> arr.fill(7)
[ 7, 7, 7 ]
> arr
[ 7, 7, 7 ]

(可选)您可以限制填充的开始和结束位置

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

18.4 ES6 和数组中的空位

空位是数组“内部”没有关联元素的索引。换句话说:如果满足以下条件,则称数组 `arr` 在索引 `i` 处有一个空位

例如:以下数组在索引 1 处有一个空位。

> const arr = ['a',,'b']
'use strict'
> 0 in arr
true
> 1 in arr
false
> 2 in arr
true
> arr[1]
undefined

在本节中,您将看到许多涉及空位的示例。如果有任何不清楚的地方,您可以查阅“Speaking JavaScript”中的“数组中的空位”一节以获取更多信息。

18.4.1 ECMAScript 6 将空位视为 `undefined` 元素

ES6 中新增的数组方法的通用规则是:每个空位都被视为元素 `undefined`。例子

> Array.from(['a',,'b'])
[ 'a', undefined, 'b' ]
> [,'a'].findIndex(x => x === undefined)
0
> [...[,'a'].entries()]
[ [ 0, undefined ], [ 1, 'a' ] ]

这样做的目的是引导人们远离空位并简化长期维护。不幸的是,这意味着现在情况更加不一致了。

18.4.2 数组操作和空位

18.4.2.1 迭代

`Array.prototype[Symbol.iterator]` 创建的迭代器将每个空位都视为元素 `undefined`。以以下迭代器 `iter` 为例

> var arr = [, 'a'];
> var iter = arr[Symbol.iterator]();

如果我们调用两次 `next()`,我们将获得索引 0 处的空位和索引 1 处的元素 `'a'`。如您所见,前者产生 `undefined`

> iter.next()
{ value: undefined, done: false }
> iter.next()
{ value: 'a', done: false }

除其他外,有两个操作基于迭代协议。因此,这些操作也将空位视为 `undefined` 元素。

首先,展开运算符 (`...`)

> [...[, 'a']]
[ undefined, 'a' ]

其次,`for-of` 循环

for (const x of [, 'a']) {
  console.log(x);
}
// Output:
// undefined
// a

请注意,数组原型方法(`filter()` 等)不使用迭代协议。

18.4.2.2 `Array.from()`

如果 `Array.from()` 的参数是可迭代的,则它使用迭代将其转换为数组。然后它的工作原理与展开运算符完全相同

> Array.from([, 'a'])
[ undefined, 'a' ]

但 `Array.from()` 也可以将类数组对象转换为数组。然后空位也变为 `undefined`

> Array.from({1: 'a', length: 2})
[ undefined, 'a' ]

使用第二个参数时,`Array.from()` 的工作原理与 `Array.prototype.map()` 大致相同。

但是,`Array.from()` 将空位视为 `undefined`

> Array.from([,'a'], x => x)
[ undefined, 'a' ]
> Array.from([,'a'], (x,i) => i)
[ 0, 1 ]

`Array.prototype.map()` 会跳过它们,但会保留它们

> [,'a'].map(x => x)
[ , 'a' ]
> [,'a'].map((x,i) => i)
[ , 1 ]
18.4.2.3 `Array.prototype` 方法

在 ECMAScript 5 中,行为已经略有不同。例如

ECMAScript 6 添加了新的行为类型

下表描述了 `Array.prototype` 方法如何处理空位。

方法 空位被  
concat 保留 ['a',,'b'].concat(['c',,'d']) → ['a',,'b','c',,'d']
`copyWithin`ES6 保留 [,'a','b',,].copyWithin(2,0) → [,'a',,'a']
`entries`ES6 元素 [...[,'a'].entries()] → [[0,undefined], [1,'a']]
every 忽略 [,'a'].every(x => x==='a') → true
`fill`ES6 填充 new Array(3).fill('a') → ['a','a','a']
filter 移除 ['a',,'b'].filter(x => true) → ['a','b']
`find`ES6 元素 [,'a'].find(x => true) → undefined
`findIndex`ES6 元素 [,'a'].findIndex(x => true) → 0
forEach 忽略 [,'a'].forEach((x,i) => log(i)); → 1
indexOf 忽略 [,'a'].indexOf(undefined) → -1
连接 元素 [,'a',undefined,null].join('#') → '#a##'
keysES6 元素 [...[,'a'].keys()] → [0,1]
lastIndexOf 忽略 [,'a'].lastIndexOf(undefined) → -1
映射 保留 [,'a'].map(x => 1) → [,1]
弹出 元素 ['a',,].pop() → undefined
推入 保留 new Array(1).push('a') → 2
归约 忽略 ['#',,undefined].reduce((x,y)=>x+y) → '#undefined'
reduceRight 忽略 ['#',,undefined].reduceRight((x,y)=>x+y) → 'undefined#'
反转 保留 ['a',,'b'].reverse() → ['b',,'a']
移出 元素 [,'a'].shift() → undefined
切片 保留 [,'a'].slice(0,1) → [,]
一些 忽略 [,'a'].some(x => x !== 'a') → false
排序 保留 [,undefined,'a'].sort() → ['a',undefined,,]
拼接 保留 ['a',,].splice(1,1) → [,]
转换为字符串 元素 [,'a',undefined,null].toString() → ',a,,'
unshift 保留 [,'a'].unshift('b') → 3
valuesES6 元素 [...[,'a'].values()] → [undefined,'a']

笔记

18.4.3 创建填充值的数组

新的 ES6 操作将空位视为 undefined 元素,这有助于创建填充值的数组。

18.4.3.1 用固定值填充

Array.prototype.fill() 用固定值替换所有数组元素(包括空位)

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

new Array(3) 创建一个包含三个空位的数组,fill() 用值 7 替换每个空位。

18.4.3.2 用升序数字填充

Array.prototype.keys() 即使数组只有空位也会报告键。它返回一个可迭代对象,您可以通过扩展运算符将其转换为数组

> [...new Array(3).keys()]
[ 0, 1, 2 ]
18.4.3.3 用计算值填充

Array.from() 的第二个参数中的映射函数会收到有关空位的通知。因此,您可以使用 Array.from() 进行更复杂的填充

> Array.from(new Array(5), (x,i) => i*2)
[ 0, 2, 4, 6, 8 ]
18.4.3.4 undefined 填充

如果您需要一个填充了 undefined 的数组,则可以使用迭代(由扩展运算符触发)将空位转换为 undefined 的事实

> [...new Array(3)]
[ undefined, undefined, undefined ]

18.4.4 从数组中删除空位

ES5 方法 filter() 允许您删除空位

> ['a',,'c'].filter(() => true)
[ 'a', 'c' ]

ES6 迭代(通过扩展运算符触发)允许您将空位转换为 undefined 元素

> [...['a',,'c']]
[ 'a', undefined, 'c' ]

18.5 配置哪些对象由 concat() 展开 (Symbol.isConcatSpreadable)

您可以通过添加一个(自身或继承的)属性来配置 Array.prototype.concat() 如何处理对象,该属性的键是众所周知的符号 Symbol.isConcatSpreadable,其值为布尔值。

18.5.1 数组的默认值:展开

默认情况下,Array.prototype.concat() 将数组*展开*到其结果中:它们的索引元素成为结果的元素

const arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e');
    // ['a', 'b', 'c', 'd', 'e']

使用 Symbol.isConcatSpreadable,您可以覆盖默认值并避免数组的展开

const arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e');
    // ['a', 'b', ['c','d'], 'e']

18.5.2 非数组的默认值:不展开

对于非数组,默认情况下不展开

const arrayLike = {length: 2, 0: 'c', 1: 'd'};

console.log(['a', 'b'].concat(arrayLike, 'e'));
    // ['a', 'b', arrayLike, 'e']

console.log(Array.prototype.concat.call(
    arrayLike, ['e','f'], 'g'));
    // [arrayLike, 'e', 'f', 'g']

您可以使用 Symbol.isConcatSpreadable 强制展开

arrayLike[Symbol.isConcatSpreadable] = true;

console.log(['a', 'b'].concat(arrayLike, 'e'));
    // ['a', 'b', 'c', 'd', 'e']

console.log(Array.prototype.concat.call(
    arrayLike, ['e','f'], 'g'));
    // ['c', 'd', 'e', 'f', 'g']

18.5.3 检测数组

concat() 如何确定参数是否为数组?它使用与 Array.isArray() 相同的算法。Array.prototype 是否在原型链中对该算法没有影响。这很重要,因为在 ES5 及更早版本中,使用了一些技巧来子类化 Array,并且这些技巧必须继续有效(请参阅本书中有关 __proto__ 的部分

> const arr = [];
> Array.isArray(arr)
true

> Object.setPrototypeOf(arr, null);
> Array.isArray(arr)
true

18.5.4 标准库中的 Symbol.isConcatSpreadable

ES6 标准库中没有对象的属性键为 Symbol.isConcatSpreadable。因此,此机制纯粹是为了浏览器 API 和用户代码而存在的。

结果

18.6 数组索引的数字范围

对于数组,ES6 仍然具有与 ES5 相同的规则

字符串和类型化数组的索引范围更大:0 ≤ i < 253−1。该范围的上限是由于 253−1 是 JavaScript 的浮点数可以安全表示的最大整数。有关详细信息,请参阅“安全整数”部分。

普通数组的索引范围较小的唯一原因是向后兼容性。

下一篇:19. 映射和集合