Array

在JS中被各种用户库扩展得最多的特性之一就是数组类型。ES6在数组上增加许多静态的和原型(实例)的帮助功能应当并不令人惊讶。

Array.of(..) 静态函数

Array(..)的构造器有一个尽人皆知的坑:如果仅有一个参数值被传递,而且这个参数值是一个数字的话,它并不会制造一个含有一个带有该数值元素的数组,而是构建一个长度等于这个数字的空数组。这种操作造成了不幸的和怪异的“空值槽”行为,而这正是JS数组为人诟病的地方。

Array.of(..)作为数组首选的函数型构造器取代了Array(..),因为Array.of(..)没有那种单数字参数值的情况。考虑如下代码:

  1. var a = Array( 3 );
  2. a.length; // 3
  3. a[0]; // undefined
  4. var b = Array.of( 3 );
  5. b.length; // 1
  6. b[0]; // 3
  7. var c = Array.of( 1, 2, 3 );
  8. c.length; // 3
  9. c; // [1,2,3]

在什么样的环境下,你才会想要是使用Array.of(..)来创建一个数组,而不是使用像c = [1,2,3]这样的字面语法呢?有两种可能的情况。

如果你有一个回调,传递给它的参数值本应当被包装在一个数组中时,Array.of(..)就完美地符合条件。这可能不是那么常见,但是它可以为你的痒处挠上一把。

另一种场景是如果你扩展Array构成它的子类,而且希望能够在一个你的子类的实例中创建和初始化元素,比如:

  1. class MyCoolArray extends Array {
  2. sum() {
  3. return this.reduce( function reducer(acc,curr){
  4. return acc + curr;
  5. }, 0 );
  6. }
  7. }
  8. var x = new MyCoolArray( 3 );
  9. x.length; // 3 -- 噢!
  10. x.sum(); // 0 -- 噢!
  11. var y = [3]; // Array,不是 MyCoolArray
  12. y.length; // 1
  13. y.sum(); // `sum` is not a function
  14. var z = MyCoolArray.of( 3 );
  15. z.length; // 1
  16. z.sum(); // 3

你不能(简单地)只创建一个MyCoolArray的构造器,让它覆盖Array父构造器的行为,因为这个父构造器对于实际创建一个规范的数组值(初始化this)是必要的。在MyCoolArray子类上“被继承”的静态of(..)方法提供了一个不错的解决方案。

Array.from(..) 静态函数

在JavaScript中一个“类数组对象”是一个拥有length属性的对象,这个属性明确地带有0或更高的整数值。

在JS中处理这些值出了名地让人沮丧;将它们变形为真正的数组曾经是十分常见的做法,这样各种Array.property方法(map(..)indexOf(..)等等)才能与它一起使用。这种处理通常看起来像:

  1. // 类数组对象
  2. var arrLike = {
  3. length: 3,
  4. 0: "foo",
  5. 1: "bar"
  6. };
  7. var arr = Array.prototype.slice.call( arrLike );

另一种slice(..)经常被使用的常见任务是,复制一个真正的数组:

  1. var arr2 = arr.slice();

在这两种情况下,新的ES6Array.from(..)方法是一种更易懂而且更优雅的方式 —— 也不那么冗长:

  1. var arr = Array.from( arrLike );
  2. var arrCopy = Array.from( arr );

Array.from(..)会查看第一个参数值是否是一个可迭代对象(参见第三章的“迭代器”),如果是,它就使用迭代器来产生值,并将这些值“拷贝”到将要被返回的数组中。因为真正的数组拥有一个可以产生这些值的迭代器,所以这个迭代器会被自动地使用。

但是如果你传递一个类数组对象作为Array.from(..)的第一个参数值,它的行为基本上是和slice()(不带参数值的!)或apply()相同的,它简单地循环所有的值,访问从0开始到length值的由数字命名的属性。

考虑如下代码:

  1. var arrLike = {
  2. length: 4,
  3. 2: "foo"
  4. };
  5. Array.from( arrLike );
  6. // [ undefined, undefined, "foo", undefined ]

因为在arrLike上不存在位置01,和3,所以对这些值槽中的每一个,结果都是undefined值。

你也可以这样产生类似的结果:

  1. var emptySlotsArr = [];
  2. emptySlotsArr.length = 4;
  3. emptySlotsArr[2] = "foo";
  4. Array.from( emptySlotsArr );
  5. // [ undefined, undefined, "foo", undefined ]

避免空值槽

前面的代码段中,在emptySlotsArrArray.from(..)调用的结果有一个微妙但重要的不同。Array.from(..)从不产生空值槽。

在ES6之前,如果你想要制造一个被初始化为在每个值槽中使用实际undefined值(不是空值槽!)的特定长数组,你不得不做一些额外的工作:

  1. var a = Array( 4 ); // 四个空值槽!
  2. var b = Array.apply( null, { length: 4 } ); // 四个 `undefined` 值

但现在Array.from(..)使这件事简单了些:

  1. var c = Array.from( { length: 4 } ); // 四个 `undefined` 值

警告: 使用一个像前面代码段中的a那样的空值槽数组可以与一些数组函数工作,但是另一些函数会忽略空值槽(比如map(..)等)。你永远不应该刻意地使用空值槽,因为它几乎肯定会在你的程序中导致奇怪/不可预料的行为。

映射

Array.from(..)工具还有另外一个绝技。第二个参数值,如果被提供的话,是一个映射函数(和普通的Array#map(..)几乎相同),它在将每个源值映射/变形为返回的目标值时调用。考虑如下代码:

  1. var arrLike = {
  2. length: 4,
  3. 2: "foo"
  4. };
  5. Array.from( arrLike, function mapper(val,idx){
  6. if (typeof val == "string") {
  7. return val.toUpperCase();
  8. }
  9. else {
  10. return idx;
  11. }
  12. } );
  13. // [ 0, 1, "FOO", 3 ]

注意: 就像其他接收回调的数组方法一样,Array.from(..)接收可选的第三个参数值,它将被指定为作为第二个参数传递的回调的this绑定。否则,this将是undefined

一个使用Array.from(..)将一个8位值数组翻译为16位值数组的例子,参见第五章的“类型化数组”。

创建 Arrays 和子类型

在前面几节中,我们讨论了Array.of(..)Array.from(..),它们都用与构造器相似的方法创建一个新数组。但是在子类中它们会怎么做?它们是创建基本Array的实例,还是创建衍生的子类的实例?

  1. class MyCoolArray extends Array {
  2. ..
  3. }
  4. MyCoolArray.from( [1, 2] ) instanceof MyCoolArray; // true
  5. Array.from(
  6. MyCoolArray.from( [1, 2] )
  7. ) instanceof MyCoolArray; // false

of(..)from(..)都使用它们被访问时的构造器来构建数组。所以如果你使用基本的Array.of(..)你将得到Array实例,但如果你使用MyCoolArray.of(..),你将得到一个MyCoolArray实例。

在第三章的“类”中,我们讲解了在所有内建类(比如Array)中定义好的@@species设定,它被用于任何创建新实例的原型方法。slice(..)是一个很棒的例子:

  1. var x = new MyCoolArray( 1, 2, 3 );
  2. x.slice( 1 ) instanceof MyCoolArray; // true

一般来说,这种默认行为将可能是你想要的,但是正如我们在第三章中讨论过的,如果你想的话你 可以 覆盖它:

  1. class MyCoolArray extends Array {
  2. // 强制 `species` 为父类构造器
  3. static get [Symbol.species]() { return Array; }
  4. }
  5. var x = new MyCoolArray( 1, 2, 3 );
  6. x.slice( 1 ) instanceof MyCoolArray; // false
  7. x.slice( 1 ) instanceof Array; // true

要注意的是,@@species设定仅适用于原型方法,比如slice(..)of(..)from(..)不使用它;它们俩都只使用this绑定(哪个构造器被用于发起引用)。考虑如下代码:

  1. class MyCoolArray extends Array {
  2. // 强制 `species` 为父类构造器
  3. static get [Symbol.species]() { return Array; }
  4. }
  5. var x = new MyCoolArray( 1, 2, 3 );
  6. MyCoolArray.from( x ) instanceof MyCoolArray; // true
  7. MyCoolArray.of( [2, 3] ) instanceof MyCoolArray; // true

copyWithin(..) 原型方法

Array#copyWithin(..)是一个对所有数组可用的新修改器方法(包括类型化数组;参加第五章)。copyWithin(..)将数组的一部分拷贝到同一个数组的其他位置,覆盖之前存在在那里的任何东西。

它的参数值是 目标(要被拷贝到的索引位置),开始(拷贝开始的索引位置(含)),和可选的 结束(拷贝结束的索引位置(不含))。如果这些参数值中存在任何负数,那么它们就被认为是相对于数组的末尾。

考虑如下代码:

  1. [1,2,3,4,5].copyWithin( 3, 0 ); // [1,2,3,1,2]
  2. [1,2,3,4,5].copyWithin( 3, 0, 1 ); // [1,2,3,1,5]
  3. [1,2,3,4,5].copyWithin( 0, -2 ); // [4,5,3,4,5]
  4. [1,2,3,4,5].copyWithin( 0, -2, -1 ); // [4,2,3,4,5]

copyWithin(..)方法不会扩张数组的长度,就像前面代码段中的第一个例子展示的。当到达数组的末尾时拷贝就会停止。

与你可能想象的不同,拷贝的顺序并不总是从左到右的。如果起始位置与目标为重叠的话,它有可能造成已经被拷贝过的值被重复拷贝,这大概不是你期望的行为。

所以在这种情况下,算法内部通过相反的拷贝顺序来避免这个坑。考虑如下代码:

  1. [1,2,3,4,5].copyWithin( 2, 1 ); // ???

如果算法是严格的从左到右,那么2应当被拷贝来覆盖3,然后这个被拷贝的2应当被拷贝来覆盖4,然后这个被拷贝的2应当被拷贝来覆盖5,而你最终会得到[1,2,2,2,2]

与此不同的是,拷贝算法把方向反转过来,拷贝4来覆盖5,然后拷贝3来覆盖4,然后拷贝2来覆盖3,而最后的结果是[1,2,2,3,4]。就期待的结果而言这可能更“正确”,但是如果你仅以单纯的从左到右的方式考虑拷贝算法的话,它就可能让人糊涂。

fill(..) 原型方法

ES6中的Array#fill(..)方法原生地支持使用一个指定的值来完全地(或部分地)填充一个既存的数组:

  1. var a = Array( 4 ).fill( undefined );
  2. a;
  3. // [undefined,undefined,undefined,undefined]

fill(..)可选地接收 开始结束 参数,它们指示要被填充的数组的一部分,比如:

  1. var a = [ null, null, null, null ].fill( 42, 1, 3 );
  2. a; // [null,42,42,null]

find(..) 原型方法

一般来说,在一个数组中搜索一个值的最常见方法曾经是indexOf(..)方法,如果值被找到的话它返回值的位置索引,没有找到的话返回-1

  1. var a = [1,2,3,4,5];
  2. (a.indexOf( 3 ) != -1); // true
  3. (a.indexOf( 7 ) != -1); // false
  4. (a.indexOf( "2" ) != -1); // false

indexOf(..)比较要求一个严格===匹配,所以搜索"2"找不到值2,反之亦然。没有办法覆盖indexOf(..)的匹配算法。不得不手动与值-1进行比较也很不幸/不优雅。

提示: 一个使用~操作符来绕过难看的-1的有趣(而且争议性地令人糊涂)技术,参见本系列的 类型与文法

从ES5开始,控制匹配逻辑的最常见的迂回方法是some(..)。它的工作方式是为每一个元素调用一个回调函数,直到这些调用中的一个返回true/truthy值,然后它就会停止。因为是由你来定义这个回调函数,所以你就拥有了如何做出匹配的完全控制权:

  1. var a = [1,2,3,4,5];
  2. a.some( function matcher(v){
  3. return v == "2";
  4. } ); // true
  5. a.some( function matcher(v){
  6. return v == 7;
  7. } ); // false

但这种方式的缺陷是你只能使用true/false来指示是否找到了合适的匹配值,而不是实际被匹配的值。

ES6的find(..)解决了这个问题。它的工作方式基本上与some(..)相同,除了一旦回调返回一个true/truthy值,实际的数组值就会被返回:

  1. var a = [1,2,3,4,5];
  2. a.find( function matcher(v){
  3. return v == "2";
  4. } ); // 2
  5. a.find( function matcher(v){
  6. return v == 7; // undefined
  7. });

使用一个自定义的matcher(..)函数还允许你与对象这样的复杂值进行匹配:

  1. var points = [
  2. { x: 10, y: 20 },
  3. { x: 20, y: 30 },
  4. { x: 30, y: 40 },
  5. { x: 40, y: 50 },
  6. { x: 50, y: 60 }
  7. ];
  8. points.find( function matcher(point) {
  9. return (
  10. point.x % 3 == 0 &&
  11. point.y % 4 == 0
  12. );
  13. } ); // { x: 30, y: 40 }

注意: 和其他接收回调的数组方法一样,find(..)接收一个可选的第二参数。如果它被设置了的话,就将被指定为作为第一个参数传递的回调的this绑定。否则,this将是undefined

findIndex(..) 原型方法

虽然前一节展示了some(..)如何在一个数组检索给出一个Boolean结果,和find(..)如何从数组检索中给出匹配的值,但是还有一种需求是寻找匹配的值的位置索引。

indexOf(..)可以完成这个任务,但是没有办法控制它的匹配逻辑;它总是使用===严格等价。所以ES6的findIndex(..)才是答案:

  1. var points = [
  2. { x: 10, y: 20 },
  3. { x: 20, y: 30 },
  4. { x: 30, y: 40 },
  5. { x: 40, y: 50 },
  6. { x: 50, y: 60 }
  7. ];
  8. points.findIndex( function matcher(point) {
  9. return (
  10. point.x % 3 == 0 &&
  11. point.y % 4 == 0
  12. );
  13. } ); // 2
  14. points.findIndex( function matcher(point) {
  15. return (
  16. point.x % 6 == 0 &&
  17. point.y % 7 == 0
  18. );
  19. } ); // -1

不要使用findIndex(..) != -1(在indexOf(..)中经常这么干)来从检索中取得一个boolean,因为some(..)已经给出了你想要的true/false了。而且也不要用a[ a.findIndex(..) ]来取得一个匹配的值,因为这是find(..)完成的任务。最后,如果你需要严格匹配的索引,就使用indexOf(..),如果你需要一个更加定制化的匹配,就使用findIndex(..)

注意: 和其他接收回调的数组方法一样,findIndex(..)接收一个可选的第二参数。如果它被设置了的话,就将被指定为作为第一个参数传递的回调的this绑定。否则,this将是undefined

entries(), values(), keys() 原型方法

在第三章中,我们展示了数据结构如何通过一个迭代器来提供一种模拟逐个值的迭代。然后我们在第五章探索新的ES6集合(Map,Set,等)如何为了产生不同种类的迭代器而提供几种方法时阐述了这种方式。

因为Array并不是ES6的新东西,所以它可能不被认为是一个传统意义上的“集合”,但是在它提供了相同的迭代器方法:entries()values(),和keys()的意义上,它是的。考虑如下代码:

  1. var a = [1,2,3];
  2. [...a.values()]; // [1,2,3]
  3. [...a.keys()]; // [0,1,2]
  4. [...a.entries()]; // [ [0,1], [1,2], [2,3] ]
  5. [...a[Symbol.iterator]()]; // [1,2,3]

就像Set一样,默认的Array迭代器与values()放回的东西相同。

在本章早先的“避免空值槽”一节中,我们展示了Array.from(..)如何将一个数组中的空值槽看作带有undefined的存在值槽。其实际的原因是,在底层数组迭代器就是以这种方式动作的:

  1. var a = [];
  2. a.length = 3;
  3. a[1] = 2;
  4. [...a.values()]; // [undefined,2,undefined]
  5. [...a.keys()]; // [0,1,2]
  6. [...a.entries()]; // [ [0,undefined], [1,2], [2,undefined] ]