Everyone,让我们来瞅瞅ES6 为 Array 增加了哪些静态函数和原型(实例)方法辅助函数,废话不多说,直接上干货!
1. 静态函数 Array.of(…)
Array(…) 构造器有一个众所周知的陷阱,就是如果只传入一个参数,并且这个参数是数字的话,那么不会构造一个值为这个数字的单个元素的数组,而是构造一个空数组,其
length 属性为这个数字。这个动作会产生不幸又诡异的“空槽”行为,这是 JavaScript 数组广为人所诟病的一点。
Array.of(…) 取代了 Array(…) 成为数组的推荐函数形式构造器,因为 Array.of(…) 并没有这个特殊的单个数字参数的问题。考虑:
let a = Array( 3 );
a.length; // 3
a[0]; // undefined
let b = Array.of( 3 );
b.length; // 1
b[0]; // 3
let c = Array.of( 1, 2, 3 );
c.length; // 3
c; // [1,2,3]
什么情况下你会需要使用 Array.of(…) 而不是只用 c = [1,2,3] 这样的字面值语法创建一个数组呢?有两种可能的情况。
如果你有一个回调函数需要传入的参数封装为数组,Array.of(…) 可以完美解决这个需求。这样的用法不是很常见,但是可能恰好“解了你的痒”。
另外一种情况是,如果你构建 Array 的子类,并且想要在你的子类实例中创建和初始化元素,比如:
class MyCoolArray extends Array {
sum() {
return this.reduce( function reducer(acc,curr){
return acc + curr;
}, 0 );
}
}
let x = new MyCoolArray( 3 );
x.length; // 3--oops!
x.sum(); // 0--oops!
let y = [3]; // Array, 而不是MyCoolArray
y.length; // 1
y.sum(); // sum不是一个函数
let z = MyCoolArray.of( 3 );
z.length; // 1
z.sum(); // 3
你不能(简单地)只是为 MyCoolArray 创建一个构造器来覆盖 Array 父构造器的行为,
因为那个构造器对于实际构造一个行为符合规范的数组值(初始化 this)是必要的。
MyCoolArray 子类“继承来的”静态 of(…) 方法提供了很好的解决方案。
2. 静态函数 Array.from(…)
JavaScript 中的“类(似)数组对象”是指一个有 length 属性,具体说是大于等于 0 的整数值的对象。
这样的值在使用 JavaScript 工作的过程中是非常令人沮丧的;普遍的需求就是把它们转
换为真正的数组,这样就可以应用各种 Array.prototype 方法(map(…)、indexOf(…) 等)
了。这个过程通常类似于:
// 类数组对象
var arrLike = {
length: 3,
0: "foo",
1: "bar"
};
var arr = Array.prototype.slice.call( arrLike );
另外一个常见的任务是使用 slice(…) 来复制产生一个真正的数组:
var arr2 = arr.slice();
两种情况下,新的 ES6 Array.from(…) 方法都是更好理解、更优雅、更简洁的替代方法:
var arr = Array.from( arrLike );
var arrCopy = Array.from( arr );
Array.from(…) 检查第一个参数是否为 iterable,如果是的话,就使用迭代器
来产生值并“复制”进入返回的数组。因为真正的数组有一个这些值之上的迭代器,所以
会自动使用这个迭代器。
而如果你把类数组对象作为第一个参数传给 Array.from(…),它的行为方式和 slice()
(没有参数)或者 apply(…) 是一样的,就是简单地按照数字命名的属性从 0 开始直到
length 值在这些值上循环。
考虑:
var arrLike = {
length: 4,
2: "foo"
};
Array.from( arrLike );
// [ undefined, undefined, "foo", undefined ]
因为位置 0、1 和 3 在 arrLike 上并不存在,所以在这些位置上是 undefined 值。
你也可以这样产生类似的结果:
var emptySlotsArr = [];
emptySlotsArr.length = 4;
emptySlotsArr[2] = "foo"; Array.from( emptySlotsArr );
// [ undefined, undefined, "foo", undefined ]
2.1 避免空槽位
前面代码中的 emptySlotArr 和 Array.from(…) 调用的结果有一个微妙但重要的区别。也就是 Array.from(…) 永远不会产生空槽位。
在 ES6 之前,如果你想要产生一个初始化为某个长度,在每个槽位上都是真正的
undefined 值(不是空槽位!)的数组,不得不做额外的工作:
var a = Array( 4 );
// 4个空槽位!
var b = Array.apply( null, { length: 4 } );
// 4个undefined值
而现在 Array.from(…) 使其简单了很多:
var c = Array.from( { length: 4 } );
// 4个undefined值
像前面代码中的 a 那样使用空槽位数组能在某些数组函数上工作,但是另外
一些会忽略空槽位(比如 map(…) 等)。永远不要故意利用空槽位工作,因为
它几乎肯定会导致程序出现意料之外的行为。
2.2. 映射
Array.from(…) 工具还有另外一个有用的技巧。如果提供了的话,第二个参数是一个映射回调(和一般的 Array#map(…) 所期望的几乎一样),这个函数会被调用,来把来自于源的
每个值映射 / 转换到返回值。
考虑:
var arrLike = {
length: 4,
2: "foo"
};
Array.from( arrLike, function mapper(val,idx){
if (typeof val == "string") {
return val.toUpperCase();
}
else {
return idx;
}
} );
// [ 0, 1, "FOO", 3 ]
和其他接收回调的数组方法一样,Array.from(…) 接收一个可选的第三个参
数,如果设置了的话,这个参数为作为第二个参数传入的回调指定 this 绑
定。否则,this 将会是 undefined。
3. 创建数组和子类型
前面几小节中,我们已经讨论了 Array.of(…) 和 Array.from(…),二者都以与构造器类似的方式创建一个新数组,而在子类型方面它们又是怎样的呢?它们会创建基类 Array 的实例还是继承子类型的实例呢?
class MyCoolArray extends Array {
..
}
MyCoolArray.from( [1, 2] ) instanceof MyCoolArray; // true
Array.from(
MyCoolArray.from( [1, 2] )
) instanceof MyCoolArray; // false
of(…) 和 from(…) 都使用访问它们的构造器来构造数组。所以如果使用基类 Array.
of(…),那么得到的就是 Array 实例;如果使用 MyCoolArray.of(…),那么得到的就是
MyCoolArray 实例。
所有的内置类(比如 Array)都有定义,任何创建新实例的原型方法都会使用它。slice(…) 是一个很好的例子:
var x = new MyCoolArray( 1, 2, 3 );
x.slice( 1 ) instanceof MyCoolArray;
// true
一般来说,默认的行为方式很可能就是需要的,必要的话也可以覆盖它:
class MyCoolArray extends Array {
// 强制species为父构造器
static get [Symbol.species]() { return Array; }
}
var x = new MyCoolArray( 1, 2, 3 );
x.slice( 1 ) instanceof MyCoolArray; // false
x.slice( 1 ) instanceof Array; // true
需要注意的是,of(…) 和 from(…)它们都只使用 this 绑定(由使用的构造器来构造其引用)。考虑:
class MyCoolArray extends Array {
// 强制species为父构造器
static get [Symbol.species]() { return Array; }
}
var x = new MyCoolArray( 1, 2, 3 );
MyCoolArray.from( x ) instanceof MyCoolArray; // true
MyCoolArray.of( [2, 3] ) instanceof MyCoolArray; // true
4. 原型方法 copyWithin(…)
Array#copyWithin(…) 是一个新的修改器方法,所有数组都支持。copyWithin(…) 从一个数组中复制一部分到同一个数组的另一个位置,覆盖这个位置所有原来的值。
参数是 target(要复制到的索引)、start(开始复制的源索引,包括在内)以及可选的 end
(复制结束的不包含索引)。如果任何一个参数是负数,就被当作是相对于数组结束的相
对值。
考虑:
[1,2,3,4,5].copyWithin( 3, 0 ); // [1,2,3,1,2]
[1,2,3,4,5].copyWithin( 3, 0, 1 ); // [1,2,3,1,5]
[1,2,3,4,5].copyWithin( 0, -2 ); // [4,5,3,4,5]
[1,2,3,4,5].copyWithin( 0, -2, -1 ); // [4,2,3,4,5]
就像前面代码片段展示的,copyWithin(…) 方法不会增加数组的长度。到达数组结尾复制就会停止。
与你想象的正相反,复制并非总是从左到右(索引递增)进行的。如果源范围和目标范围
重叠的话,可能会出现重复复制已经复制的值,而这可能并非你想要的结果。
所以,内部算法通过反向复制避免了这种情况。考虑:
[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]。根据期望来说,这可能是更“正确”的结果,但如果只考虑简单的从左到右方式的复制算法,你可能会觉得很迷惑。
5. 原型方法 fill(…)
可以通过 ES6 原生支持的方法 Array#fill(…) 用指定值完全(或部分)填充已存在的数
组:
var a = Array( 4 ).fill( undefined );
a;
// [undefined,undefined,undefined,undefined]
fill(…) 可选地接收参数 start 和 end,它们指定了数组要填充的子集位置,比如:
var a = [ null, null, null, null ].fill( 42, 1, 3 );
a; // [null,42,42,null]
6. 原型方法 find(…)
一般来说,在数组中搜索一个值的最常用方法一直是 indexOf(…) 方法,这个方法返回找到值的索引,如果没有找到就返回 -1:
var a = [1,2,3,4,5];
(a.indexOf( 3 ) != -1); // true
(a.indexOf( 7 ) != -1); // false
(a.indexOf( "2" ) != -1); // false
相比之下,indexOf(…) 需要严格匹配 ===,所以搜索 “2” 不会找到值 2,反之也是如此。
indexOf(…) 的匹配算法无法覆盖,而且要手动与值 -1 进行比较也很麻烦 / 笨拙。用 ~ 运算符来绕过丑陋的返回值 -1 的问题。
从 ES5 以来,控制匹配逻辑的最常用变通技术是使用 some(…) 方法。它的实现是通过为每个元素调用一个函数回调,直到某次调用返回 true / 真值时才会停止。因为你可以定义这个回调函数,也就有了对匹配方式的完全控制:
var a = [1,2,3,4,5];
a.some( function matcher(v){
return v == "2";
} ); // true
a.some( function matcher(v){
return v == 7;
} ); // false
但这种方式的缺点是如果找到匹配的值的时候,只能得到匹配的 true/false 指示,而无法得到真正的匹配值本身。
ES6 的 find(…) 解决了这个问题。基本上它和 some(…) 的工作方式一样,除了一旦回调返回 true/ 真值,会返回实际的数组值:
var a = [1,2,3,4,5];
a.find( function matcher(v){
return v == "2";
} ); // 2
a.find( function matcher(v){
return v == 7; // undefined
});
通过自定义 matcher(…) 函数也可以支持比较像对象这样的复杂值:
var points = [
{ x: 10, y: 20 },
{ x: 20, y: 30 },
{ x: 30, y: 40 },
{ x: 40, y: 50 },
{ x: 50, y: 60 }
];
points.find( function matcher(point) {
return (
point.x % 3 == 0 &&
point.y % 4 == 0
);
} ); // { x: 30, y: 40 }
就像其他接受回调的数组方法一样,find(…) 接受一个可选的第二个参
数,如果设定这个参数就绑定到第一个参数回调的 this。否则,this 就是
undefined。
7. 原型方法 findIndex(…)
前面一小节展示了 some(…) 如何 yield 出一个布尔型结果用于在数组中搜索,以及
find(…) 如何从数组搜索 yield 出匹配的值本身,另外,还需要找到匹配值的位置索引。
indexOf(…) 会提供这些,但是无法控制匹配逻辑;它总是使用 === 严格相等。所以 ES6的 findIndex(…) 才是解决方案:
var points = [
{ x: 10, y: 20 },
{ x: 20, y: 30 },
{ x: 30, y: 40 },
{ x: 40, y: 50 },
{ x: 50, y: 60 }
];
points.findIndex( function matcher(point) {
return (
point.x % 3 == 0 &&
point.y % 4 == 0
);
} ); // 2
points.findIndex( function matcher(point) {
return (
point.x % 6 == 0 &&
point.y % 7 == 0
);
} ); // -1
不要使用 findIndex(…) != -1(这是 indexOf(…) 的惯用法)从搜索中得到布尔值,
因为 some(…) 已经 yield 出你想要的 true/false。也不要用 a[ a.findIndex(…) ] 来得到匹配值,因为这是 find(…) 所做的事。最后,如果需要严格匹配的索引值,那么使用indexOf(…);如果需要自定义匹配的索引值,那么使用 findIndex(…),就像其他接收回调的数组方法一样,findIndex(…) 接收一个可选的第二个参数,如果设定这个参数就绑定到第一个参数回调的 this。否则,this 就是undefined。
8. 原型方法 entries()、values()、keys()
Array 对于 ES6 来说已经不是新的了,所以从传统角度来说,它可能不会被看作是
“集合”,但是它提供了同样的迭代器方法 entries()、values() 和 keys(),从这个意义上说,它是一个集合。考虑:
let a = [1,2,3];
[...a.values()]; // [1,2,3]
[...a.keys()]; // [0,1,2]
[...a.entries()]; // [ [0,1], [1,2], [2,3] ]
[...a[Symbol.iterator]()]; // [1,2,3]
就像 Set 一样,默认的 Array 迭代器和 values() 返回的值一样。
Array.from(…) 如何把数组中的空槽位看作值为undefined 的槽位。这实际上是因为在底层数组迭代器是这样工作的:
let a = [];
a.length = 3;
a[1] = 2;
[...a.values()]; // [undefined,2,undefined]
[...a.keys()]; // [0,1,2]
[...a.entries()]; // [ [0,undefined], [1,2], [2,undefined] ]
总结
不管是在学习,还是日常开发中,Array的都是非常重要的一部分,只有把它学懂,吃透,我们才能在这条路上越走越远。欢迎童鞋们,在留言区多多互动,提出您的宝贵意见!后续还会为大家更新前端实战中一些很重要的知识点,助你升级打怪!