听风是风

学或不学,知识都在那里,只增不减。

导航

es6入门6--数组拓展运算符,Array.from()基本用法

本文只是作为ES6入门第九章学习笔记,在整理知识点的同时,会加入部分个人思考与解答,若想知道更详细的介绍,还请阅读阮一峰大神的ES6入门

一、拓展运算符

ES6中新增了拓展运算(...)三个点,它的作用是将一个数组或实现了Iterator接口的对象/类数组(nodeList,arguments)转为分隔的参数序列。

console.log(...['echo', '听风是风', '时间跳跃']); //echo 听风是风 时间跳跃

类数组arguments使用拓展运算符:

let fn = function () {
    console.log(...arguments);
};
fn(1, 2, 3, 4); //1,2,3,4

数组拓展运算符与函数rest参数的区别:

比较巧的是,其实在ES6入门第八章函数拓展中,也有用三个小点且用于取代arguments的rest参数的rest参数。为了避免混淆两者,我们做个简单区分。

let fn = function (...rest) {
    rest.forEach(ele => console.log(ele));
};
fn('听风是风', ...[1, 2, 3]); //听风是风 1 2 3

//等同于
fn('听风是风', 1, 2, 3);

rest参数作用是取代arguments,用于函数参数上,在函数内部使用rest参数时不需要带上...;数组拓展运算符更多用于函数调用上,且...必须带上

//rest参数
let fn = function (...rest) {
    //使用时...不需要带上
    rest.forEach(ele => console.log(ele));
};

//数组拓展运算符
let arr = [1,2,3];
//使用时...永不离身
Math.max(...arr);//3

rest参数必须写在函数参数末尾,否则标错;数组拓展运算符在函数调用时可以出现在任意位置,但需要注意的是只有函数调用时才能放在圆括号中,否则报错。

//错误用法,...rest只能出现在参数末尾
let fn = function (name, ...rest, age) {};

//错误用法,除非是函数调用,否则不能写在花括号中
(...[1,2,3]);

 二、拓展运算符的作用

1.取代apply方法

实际开发中我们常常需要通过apply或者call方法来调用某方法,但如果调用时参数是一个数组,就不得不使用apply,因为apply能将数组解析给单个参数传递给函数。

而拓展运算符刚好就具有将数组,类数组转为单个元素序列的功能,因此能很好的取代apply。比如函数应用:

let fn = (x, y, z) => console.log(x + y + z),
    arr = [1, 2, 3];
//函数应用
//ES5
fn.apply(null, arr); //6
// ES6
fn(...arr); //6

还记得常用的数组取最大,最小值的方法吗。Math.max同样能够简写:

let arr = [1, 2, 3, 4, 5];
//ES5
Math.max.apply(null, arr); //5
Math.min.apply(null, arr); //1
//ES6
Math.max(...arr); //5
Math.min(...arr); //1

将一个数组的元素压入到另一个数组尾部(我之前都没想过用这个做法...):

let arr1 = [1, 2, 3],
    arr2 = [4, 5, 6];
//ES5
Array.prototype.push.apply(arr1, arr2);
//ES6
arr1.push(...arr2);

2.合并/复制数组:

let arr1 = [1, 2, 3],
    arr2 = [4, 5, 6],
    arr3 = [7, 8, 9];
//ES5
let arr = arr1.concat(arr2, arr3);
//ES6
let arr = [...arr1, ...arr2, ...arr3];

顺带一提,concat方法不会修改原数组,而是复制数组中的元素组成并返回一个新数组,concat和拓展运算符合并数组的操作都是浅拷贝

你肯定会想,不是说concat是浅拷贝吗,为啥我下面的修改合并的新数组没跟这变化:

let arr1 = [1, 2, 3],
    arr2 = [4, 5, 6];
//ES5
let arr = arr1.concat(arr2);
arr1.push(10);
console.log(arr); //[1,2,3,4,5,6]

这是因为concat虽然操作的是数组,但是复制添加的不是整个数组,而是数组中的单个元素们,上述数组中的元素均为number类型,属于基础类型。

let arr1 = [{a:1}, {a:2}],
    arr2 = [{c:3}, {d:4}];
//ES5
let arr = arr1.concat(arr2);
arr1[0]['a'] = 5;
console.log(arr); //[{a:5}, {a:2},{c:3}, {d:4}]

那我们将数组中的单个元素改为对象试试,可以看到修改arr1时会对arr数组造成影响,拓展运算符合并数组同为浅拷贝也是这个道理。

ES6入门这本书中也提到使用拓展运算符复制数组的操作,同样需要注意当元素是复杂数组类型会存在浅拷贝的问题。

let arr = [1,2,3,4];
//以下两种写法复制数组等效
arr1 = [...arr];
[...arr1] = arr;

 3.结合解构赋值实现快速赋值

我在ES6入门笔记第二篇中有记录解构赋值的使用,解构赋值同样能与数组拓展运算符结合使用,达到快速拆分数组分开赋值的目的。

let arr = [1, 2, 3, 4];
[a, ...b] = arr;
a//1
b//[2,3,4]

对应位置去看,其实就是将数组第一位给了a,剩下的元素打包给了b,不难理解。

下面是数组元素不足,或者为空时使用解构赋值的情况:

let arr = [];
[a, ...b] = arr;
a //undefined
b //[]

let arr1 = ['听风是风'];
[a1, ...b1] = arr1;
a1 //'听风是风'
b1 //[]

最后需要注意的一点是,如果结合解构赋值使用,拓展运算符必须写在参数最后一位,否则会报错。还记得吗,...rest参数也是必须写在函数参数最后一位。

let arr = [];
[...a, b] = arr; //报错

4.类数组转数组

在前面已经有提过,类数组(例如nodeList,arguments)结合拓展运算符能快速转为数组:

let P = document.querySelectorAll('p');
//获取页面所有p元素并修改文本内容
[...P].forEach(ele => ele.innerHTML = '听风是风');

5.将字符串快速转为数组

拓展运算符可以将字符串转为字符串使用

let name = 'echo';
//ES5
let name1 = name.split('');
// ES6
let name1 = [...name];
name1 //['e','c','h','o']

6.实现了Iterator接口的对象都能使用拓展运算符

拓展运算符解析原理其实就是内部调用了目标对象的遍历器接口(Symbol.iterator),只要具备Iterator接口的对象,都可以使用拓展运算符:

let map = new Map([
    ['听风是风', 1],
    ['echo', 2],
    ['时间跳跃', 3]
]);
[...map.keys()] //['听风是风','echo','时间跳跃']

二、Array.from()方法

1.基本用法

Array.from方法主要用途是将类数组(具有length属性)对象,或可遍历的对象转为真正的数组。

我们在前面已经使用拓展运算符将类数组nodeList和arguments转为了真正的数组,这里Array.from方法同样能做到。你肯定会想,既然拓展运算符可以做到,为什么还要新增这个方法,因为两者还是有区别的。

类数组对象是具有length属性的对象,nodeList,和arguments具有length属性的同时,其实还具有Iterator接口,所以拓展运算符才能调用Iterator接口转换。

比如下面两个类数组对象,都具有length属性但不具备iterator接口,Array.from方法可以转为数组,但拓展运算符不能做到:

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};
// ES5
let arr = Array.prototype.slice.call(arrayLike);
// ES6
let arr1 = Array.from(arrayLike);
arr; //['a','b','c'];
arr1; //['a','b','c'];
//拓展运算符无法转换不具有iterable接口的对象
[...arrayLike] //报错arrayLike is not iterable

再如只有length属性的类数组对象:

let obj = {
    length: 3
};
Array.from(obj); //[undefined,undefined,undefined]
//同样拓展运算符无法转换
[...obj]; //报错

2.Array.from()方法的第二个参数

Array.from()方法的第二个参数类似与数组的map方法,用于对每个元素进行处理,并将处理后的结果放入返回的数组。

let arrayLike = {
    '0': 1,
    '1': 2,
    '2': 3,
    length: 3
};
// ES6
let arr1 = Array.from(arrayLike, ele => ele + 1); //[2,3,4]
// ES5
let arr2 = Array.prototype.map.call(arrayLike, ele => ele + 1); //[2,3,4]
// 等同于
let arr3 = Array.from(arrayLike).map(ele => ele + 1); //[2,3,4];

比如快速生成包含10个数字1的数组:

let arrayLike = {length: 10};
let arr = Array.from(arrayLike, () => 1);

三、Arrar.of()方法

Array.of()用于将一组值转为为数组,主要目的是弥补构造函数Array()的不足:

对于创建数组,我们常推荐数组直接量,而非使用数组构造函数创建,因为对于特殊情况会得到你不想要的值:

let arr = [3]; //[3]
//构造函数的坑
let arr1 = Array(3); //[undefined,undefined,undefined]
//Array.of可以弥补这点
let arr3 = Array.of(3) //[3]

如果不支持此方法,我们也可以模拟实现:

let ArrayOf = function () {
    if (Array.of) {
        return Array.of(...arguments);
    } else {
        return [].slice.call(arguments);
    };
};
let arr = ArrayOf(1, 2, 3); //[1,2,3]

四、数组实例新增API

此方法用于数组复制指定位置开始到指定位置结束的成员到其它位置,修改原数组并返回。

Array.prototype.copyWithin(target, start = 0, end = this.length)
·target(必需):从该位置开始替换数据。如果为负值,表示倒数。
·start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
·end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数(也就是说复制不包含这一位)

看几个例子:

let arr = [1, 2, 3, 4, 5];
//正数下标  0  1  2  3  4
//负数下标 -5 -4 -3 -2 -1
//从下标2开始到下标3结束,但不包含下标3的元素,所以只有元素3,并复制到下标0处替换原有的元素
[1, 2, 3, 4, 5].copyWithin(0, 2, 3); //[3,2,3,4,5]
//从下标-3开始到下标-1结束,但不包含下标-1的元素,所以只有元素3,4,并复制到下标0处替换原有的元素 
[1, 2, 3, 4, 5].copyWithin(0, -3, -1); //[3,4,3,4,5]

需要注意的是,在使用copyWidthin方法时,你的start元素位置必须在end元素位置左边,否则此方法将无效:

let arr = [1, 2, 3, 4, 5];
//起始元素均在结束元素右边,不会起作用
arr.copyWithin(0, 3, 2); //[1, 2, 3, 4, 5] 
arr.copyWithin(0, -1, -3); //[1, 2, 3, 4, 5]

 2.find()与findIndex()

find方法在开发中使用频率很高,它能帮你找到第一个符合条件的对象并返回,并且跳出循环,如果未找到则返回undefined。

let arr = [1, 2, 3, 4, 5];
var ele = arr.find(ele => ele > 3);
ele //4

find方法的回调函数接受三个参数:

arr.find(function (ele, index, self) {});
·ele代表循环的当前项
·index代表当前项的下标
·self代表被循环的原数组对象

findIndex用法与find一样,只是它会返回第一个符合条件的元素下标位置,回调函数同样拥有三个参数,用法与find相同,这里就不做介绍了。

3.fill()方法

数组的一个填充方法,接受三个参数,分别代表填充所用元素,填充起始位置,填充结束位置(填充时不包含此位置)

[1, 2, 3].fill('听风是风', 1, 2); //[1,'听风是风',3]
new Array(3).fill('echo'); //['echo','echo','echo']

前面我们有使用Array.from方法快速生成包含10个1的数组,那么到这里,你又知道了可以使用fill快速生成相同元素的数组。

需要注意的是,当我们使用fill填充的元素是对象时,会存在浅拷贝的问题,也就是说,当你填充后修改数组任意下标的对象,其它下标的元素都会改变

let arr = new Array(3).fill({
    name: 'echo'
});
arr[0].name = '听风是风';
arr //[{name:'听风是风'},{name:'听风是风'},{name:'听风是风'}]

4.keys(),value()与entries()方法

这三个方法均用于遍历数组,这三个都返回一个遍历器对象,可以结合for...of循环分别对数组键名(keys()),数组键值(value()),数组键值对(entries())进行遍历。

let arr = [{name:'听风是风'},{age:26},{address:'深圳'}];
for(let index of arr.keys()){
    console.log(index);//0,1,2
};
for(let ele of arr.values()){
    console.log(ele);//{name:'听风是风'},{age:26},{address:'深圳'}
};
for(let [index,value] of arr.entries()){
    console.log(index,value);//0 {name:'听风是风'},1 {age:26},2 {address:'深圳'}
};

请仔细看这个例子,我原以为keys遍历的是数组元素的name,age,adress,values遍历出来的是听风是风,26与深圳,其实并不是。

不要被方法名骗了,keys遍历仅仅是数组的下标,values遍历出来的是单个元素,entries遍历出来的是下标与元素的对。

如果不使用for...of循环,可以使用遍历器对象next方法进行遍历,由于我还没看这一章对next不太了解,这里先做个记录。

let arr = ['a','b','c'];
let index = arr.keys();
console.log(index.next().value);//0
console.log(index.next().value);//1
console.log(index.next().value);//2

5.includes方法

此方法用于判断数组是否包含某个值,返回一个Boolean值,包含返回true,不包含则返回false。

let arr = [1,2,3];
//ES6
arr.includes(1);//true
//ES5 使用indexOf,并判断返回的索引是否大于-1
arr.indexOf(1)//0

在这个方法之前我们习惯使用indexOf查找,找到指定元素返回该元素索引,找不到返回-1。但需要注意的是,indexOf无法查找NaN这个特例:

let arr = [NaN];
arr.includes(NaN);//true
arr.indexOf(NaN)//-1

我们也可以使用some方法自己模拟查找方法,

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

6.数组的flat()与flatMap()方法

flat方法可以将多维数组转变为一维数组并返回一个新数组,不会修改原数组,也是非常的骚操作了:

let arr = [1, 2, 3, [4]];
arr.flat(); //[1,2,3,4]

那么要是数组维度超过2层怎么办呢,flat方法接受一个参数,定义该方法要降维的层数:

let arr = [1, 2, 3, [4,[5]]];
arr.flat(2); //[1,2,3,4,5]

如果你不想知道数组嵌套了多少层,不知道这个参数定义多少,你可以直接使用infinite代替,这样不管几层都会被降为一维数组:

let arr = [1,[2,[3,[4,[5]]]]];
arr.flat(Infinity); //[1,2,3,4,5]

flatMap()方法对数组中每个元素执行一个回调函数(类似map方法),然后使用返回值组成一个新数组,不会修改原数组,最后再对这个新数组执行flat方法。

需要注意的是flatMap方法最多只能展开二维数组:

let arr = [1, [2, [3, [4, [5]]]]];
let arr1 = [1, 2, 3, [4]];
//超过二维就无法解析了
let arr_ = arr.flatMap(ele => ele * 2); //[1,2,3,4,5]
//二维数组还是可以正常计算
let arr1_ = arr1.flatMap(ele => ele * 2); //[1,2,3,4,5]
console.log(arr_); //[2,NaN]
console.log(arr1_); //[2,4,6,8]

//返回的新数组确实还会被flat方法降维一次
let arr3 = [1, 2, 3];
let arr3_ = arr3.flatMap(ele => [ele * 2]);
console.log(arr3_); //[2,4,6]

flatMap回调函数同样接受三个参数,与前面find方法参数意义相同。

posted on 2019-06-02 23:56  听风是风  阅读(1765)  评论(0编辑  收藏  举报