ES6数组的扩展

Array.from()



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

常用传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Array.from('hello') // [ 'h', 'e', 'l', 'l', 'o' ]

Array.from([1, 2, 3]) // [ 1, 2, 3 ]

// 传入值为Map类型时和传入值为Set时类似
let namesSet = new Set(['a', 'b']);
Array.from(namesSet) // [ 'a', 'b' ]

// 传参为无length属性的Object。
var o = { 0: 'a', 1: 'b', 2: 'c' }
Array.from(o) // []

// 传参为有length属性的Object。
var o = { 0: 'a', 1: 'b', 2: 'c', length: 3 }
Array.from(o) // [ 'a', 'b', 'c' ]

通过对比可以得知一个结论:

任何有length属性的对象,都可以通过Array.from方法转为数组。

原本让我用任何这个限定词的时候我是拒绝的,但是阮一峰老湿的文章《数组的扩展》里面是这么说的。I trust him completely.

传参为类数组对象

什么是类数组对象

我只是概述一下我们这篇文章需要的类数组对象的芝士点(在我的世界,芝士和知识同义)。

类数组对象实现了遍历器接口Symbol.iterator,它有length属性,数据结构和数组很像,但是它不是数组。你看,多神奇?!

我不是芝士的缔造者,我只是大自然的搬运工。具体的可以参考文章《Array-like Objects in JavaScript》

扩展运算符的异曲同工

1
2
3
4
function foo() {
Array.from(arguments); // [ 'a', 'b', 'c' ]
}
foo('a', 'b', 'c');

扩展运算符(…)也可以将某些数据结构转为数组。

补充

有个细节。... 运算符的作用其实是展开一个对象,然后配合[]才可以转成数组。... 运算符并不能直接转成数组。

这里先感谢zhouyg的指正。我直接引用原话,因为他的表达很简洁易懂。

1
2
3
4
function foo() {
[...arguments]; // [ 'a', 'b', 'c' ]
}
foo('a', 'b', 'c');

上面的某些操作符被我@了,这说明拓展操作符(…)是有局限性的。引用阮一峰老师的原话是这样的:

扩展运算符背后调用的是遍历器接口Symbol.iterator,如果一个对象没有部署这个接口,就无法转换。

1
2
3
var o = { 0: 'a', 1: 'b', 2: 'c', length: 3 }
Array.from(o) // [ 'a', 'b', 'c' ]
[...o] // TypeError

因为给Object o定义了length属性,所以Array.from函数才得以生效。但是Object o说白了仅仅只是get到了length技能,但还是没有实现遍历器接口Symbol.iterator,所以拓展操作符(…)就略显无力了。

既然说了这么多就再说一些

我只发代码,我先不说话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// key值为整形
var o = { 0: 'a', 1: 'b', 2: 'c', length: 3 }
Array.from(o) // [ 'a', 'b', 'c' ]

// key值为只包含数字内容的字符串
var o = { '0': 'a', '1': 'b', '2': 'c', length: 3 }
console.log(Array.from(o)); // [ 'a', 'b', 'c' ]

// key值不从0起
var o = { 1: 'a', 2: 'b', 3: 'c', length: 3 }
Array.from(o) // [ undefined, 'a', 'b' ]

// key值为不能转化为数字的字符串
var o = { a: 'a', b: 'b', c: 'c', length: 3 }
Array.from(o) // [ undefined, undefined, undefined ]

// key值为包含数字和字符混杂的形式
var o = { '0a': 'a', '1': 'b', '2': 'c', length: 3 }
console.log(Array.from(o)); // [ undefined, 'b', 'c' ]

// key值既有字符又有数字
var o = { a: 'a', 2: 'b', c: 'c', length: 3 }
console.log(Array.from(o)); // [ undefined, undefined, 'b' ]

// key值既有字符又有数字并且length属性大于对象除length属性外定义属性的个数
var o = { a: 'a', 2: 'b', c: 'c', length: 4 }
Array.from(o) // [ undefined, undefined, 'b', undefined ]

代码发完了,是不是都很相似,眼花了有木有?!我把上面的代码结果总结一下吧。

Array.from是通过key作为下标并且参照length属性大小来填充元素的。当没有合适的元素插入时就会插入undefined补位。

兼容性

对于还没有部署该方法的浏览器,可以使用Array.prototype.slice方法替代。
1
const toArray = (() => Array.from ? Array.from : obj => Array.prototype.slice.call(obj))();

写的很精简有木有,但是可能会让人觉得一下子不太好消化。

1
2
3
4
5
const toArray = (function() {
return Array.from ? Array.from : function(obj) {
return Array.prototype.slice.call(obj);
};
})();

这段代码和上段代码有异曲同工之妙,会不会好理解一些呢?如果觉得还是不太好理解,没事儿,咱再看一段代码。

1
2
3
4
5
6
7
8
9
const toArray = (function() {
if (Array.from) {
return Array.from;
} else {
return function(obj) {
return Array.prototype.slice.call(obj);
}
}
})();

看到这个简单明了了吧,如果还是有什么疑问的话,那只能说,妈妈叫你回家学基础了。

类似map()的用法

1
2
3
4
5
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.from([1, 2, 3], (x) => x * x) // [ 1, 4, 9 ]

下面的例子将数组中布尔值为false的成员转为0。

1
Array.from([1, , 2, , 3], (n) => n || 0) // [1, 0, 2, 0, 3]

Array.from()可以将各种值转为真正的数组,并且还提供map功能。这实际上意味着,你可以在数组里造出任何想要的值。

1
Array.from({ length: 2 }, () => 'jack') // ['jack', 'jack']

上面代码中,Array.from的第一个参数指定了第二个参数运行的次数。这种特性可以让该方法的用法变得非常灵活。

Array.from()的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种Unicode字符,可以避免JavaScript将大于\uFFFF的Unicode字符(超纲字),算作两个字符的bug。

1
2
3
function countSymbols(string) {
return Array.from(string).length;
}

Array.of()


Array.ofArray()的异同

Array.of的行为很统一。
1
2
3
4
Array.of() // []
Array.of(3) // [ 3 ]
Array.of(3, 11, 8) // [ 3, 11, 8 ]
Array.of(undefined) // [undefined]

当传入单个参数时,Array()的行为表现的不统一。

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

童鞋们知道当传入单个参数时,Array是怎么处理的么?生成的数组的元素都是什么类型的呢?既然之前学了Array.from类似map()方法的运用,这里怎么的也得实战一下子啊。

1
2
3
4
5
function typesof() {
return Array.from(arguments, v => typeof v);
}

typesof(...Array(3)) // [ 'undefined', 'undefined', 'undefined' ]

这里还顺带了操作扩展符(…)逆转化数组的芝士点。不懂的童鞋看看阮一峰老湿的《函数的扩展》吧。请叫我雷锋。

兼容性

Array.of方法可以用下面的代码模拟实现。
1
2
3
function ArrayOf() {
return [].slice.call(arguments);
}

数组实例的copyWithin()


常用方式

数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
1
Array.prototype.copyWithin(target, start = 0, end = this.length)

它接受三个参数。

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

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

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

上面代码表示将从3号位直到数组结束的成员(4和5),复制到从0号位开始的位置,结果覆盖了原来的1和2。
下面是更多例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4) // [ 4, 2, 3, 4, 5 ]

// 上面和下面这两个方法等同

// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1) // [ 4, 2, 3, 4, 5 ]

// 将3号位复制到0号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3) // { '0': 1, '3': 1, length: 5 }

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

兼容性

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

多说几句

既然使用了copuWithin方法会修改当前数组,那么我们就总结一下还有哪些方法也会更改当前数组,哪些则不会。只列取常用的,贵精不贵全。

不会修改当前数组的方法

concat()方法用于连接两个或多个数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。

1
2
3
4
var arr = Array.of(1, 2, 3);
var resArr = arr.concat(4);
arr // [ 1, 2, 3 ]
resArr // [ 1, 2, 3, 4 ]

join()方法用于把数组中的所有元素放入一个字符串。元素是通过指定的分隔符进行分隔的。

1
2
3
4
var arr = Array.of(1, 2, 3);
var resStr = arr.join(",");
arr // [ 1, 2, 3 ]
resStr // 1,2,3

会修改当前数组的方法

pop()方法用于删除并返回数组的最后一个元素。

1
2
3
4
var arr = Array.of(1, 2, 3);
var resEle = arr.pop();
arr // [ 1, 2 ]
resEle // 3

push()方法可向数组的末尾添加一个或多个元素,并返回新的长度。

1
2
3
4
var arr = Array.of(1, 2, 3);
var resLen = arr.push('a');
arr // [ 1, 2, 3, 'a' ]
resLen // 4

sort()方法用于对数组的元素进行排序。

1
2
3
4
var arr = Array.of(3, 11, 8);
var res = arr.sort();
arr // [ 11, 3, 8 ]
res // [ 11, 3, 8 ]

结果是不是很意外,没错,排序并不是按整型大小,而是字符串对比,就是取第一个字符的ANSI码对比,小的排前面,相同的话取第二个字符再比,如果要按整型数值比较,可以这样。

1
2
3
4
var arr = Array.of(3, 11, 8);
var res = arr.sort((a, b) => a - b);
arr // [ 3, 8, 11 ]
res // [ 3, 8, 11 ]

reverse()方法用于颠倒数组中元素的顺序。

1
2
3
4
var arr = Array.of(3, 11, 8, 9);
var res = arr.reverse();
arr // [ 9, 8, 11, 3 ]
res // [ 9, 8, 11, 3 ]

数组实例的find()findIndex()



数组实例的find方法,用于找出第一个符合条件的数组成员。

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

1
[1, 4, -5, 10].find((n) => n < 0) // -5

上面代码找出数组中第一个小于0的成员。

1
2
3
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10

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

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

数组实例的fill()



fill方法使用给定值,填充一个数组。
1
2
3
['a', 'b', 'c'].fill(7) // [7, 7, 7]

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

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

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

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

凡是带这种传参有startIndexendIndex的方法,都是以startIndex开始,以endIndex为结束但不包含endIndex元素上的值。