一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第13天,点击查看活动详情。
十四、ES6+数组方法
14-1、Array.from
类数组不能使用数组的相关方法,比如push、pop等,此时我们就要使用到Array.from方法来将类数组转换为数组。
- Array.from会接收一个类数组对象然后返回一个数组实例,返回的数组实例可以调用数组的所有方法
语法:
Array.from(arrayLike[, mapFn[, thisArg]])
参数说明:
参数 | 描述 |
---|---|
arrayLike | 想要转换成数组的类数组对象/可迭代对象 |
mapFn | (可选)如果传递了该参数,那么新数组中的每个元素都会执行此回调函数 |
thisArg | (可选)执行回调函数mapFn时的this指向 |
14-1-1、ES5中类数组转换数组的方式
所谓的类数组对象,指的是可以通过索引来访问元素,并且对象拥有length属性,类数组一般是如下结构:
const arrLike = {
'0': 'apple',
'1': 'banana',
'2': 'orange',
length: 3
};
在ES5中,没有对应的方法来将类数组转换为数组,一般是借助call / apply方法来实现:
const arr1 = [].slice.call(arrLike);
// 或
const arr2 = [].slice.apply(arrLike);
14-1-2、Array.from的基本使用方法
- 基本使用
const arrLike = {
'0': 'apple',
'1': 'banana',
'2': 'orange',
length: 3
};
const arr = Array.from(arrLike);
console.log(arr) // ['apple', 'banana', 'orange']
- 第二个参数传递回调函数
在Array.from中第二个参数是以一个类似map的回调函数
- 该回调函数会依次接收数组中的每一项,然后对传入的值执行函数中的逻辑,得到一个新的数组
- 当然,我们也可以在使用Array.from转为数组后再进行map操作以达到同样的目的
const arr1 = Array.from([1, 2, 3], function (x) {
return 2 * x;
});
const arr2 = Array.from([1, 2, 3]).map(function (x) {
return 2 * x;
});
//arr1: [2, 4, 6] arr2: [2, 4, 6]
- 传递第三个参数
Array.from中传递第三个参数,可以对第二个参数的回调函数进行this指向的绑定
- 我们可以通过传递第三个参数来将不同的处理数据的方法封装到不同的对象中以达到解耦的目的
const obj = {
handle: function(n){
return n + 2
}
}
Array.from([1, 2, 3, 4, 5], function (x){
return this.handle(x)
}, obj)
// [3, 4, 5, 6, 7]
- 以上代码中,将obj作为第三个参数传递给Array.from,那么回调函数中的this变指向了obj,这样便可以在回调函数中通过this.handle来获取到obj中定义的handle方法啦
14-1-3、Array.from的使用场景
- 将字符串转换为数组
console.log(Array.from('abcde')); // [ "a", "b", "c", "d", "e" ]
- 将Set转换为数组
const set = new Set(['a', 'b', 'c', 'd']);
console.log(Array.from(set)); // [ "a", "b", "c", "d" ]
- 将Map转换为二维数组
const map = new Map([[1, 2], [2, 4], [4, 8]]);
Array.from(map); // [[1, 2], [2, 4], [4, 8]]
14-2、Array.of
ES5中我们创建数组实例要么使用字面量的方式,要么通过new Array()来创建,但是new Array()有不足之处:
- new Array() 会因为传递参数个数的不同,而执行不同的重载函数
而ES6新增的Array.of()方法不会因为参数个数不同而导致的重载
14-2-1、ES5中的new Array()
- 不传递参数
使用new Array不传递参数时会返回一个空数组
console.log(new Array()); // []
- 传递一个参数
传递一个参数时,表示要创建的数组的长度,则会返回指定长度的空数组,并且该数组不可以被迭代
console.log(new Array(3)); // [,,]
- 传递多个参数
传递2个或2个以上的多个参数时,才会将参数当成数组的每一项返回
console.log(new Array(3,2,1)); // [3,2,1]
所以,new Array在构造数组对象时,在没有参数、传递一个参数、多个参数时返回结果都不一样:
- 没有参数时返回一个空数组
- 有一个参数的时候,返回一个以此参数为长度的空数组,并且该数组不能被迭代
- 有两个或两个以上参数时,才会把传递的参数作为构造出来的数组对象中的每一项
14-2-2、Array.of基本使用方法
语法:
Array.of(element0[, element1[, ...[, elementN]]])
参数说明:
参数 | 描述 |
---|---|
elementN | 数组元素,可以是任意个数,会按照顺序成为返回数组中的元素 |
Array.of(7); // [7]
Array.of(1, 2, 3); // [1, 2, 3]
Array.of(undefined) // [undefined]
Array.of(3).length // 1
如果不支持Array.of,我们也可以手动实现:
if (!Array.of) {
Array.of = function() {
return Array.prototype.slice.call(arguments);
};
}
14-3、includes
该方法用于查找数组中是否存在某元素
- ES5中,我们往往通过indexOf方法来查询数组中是否存在某元素,但indexOf方法存在一定缺陷
- 而includes是为了取代indexOf而设计的
14-3-1、ES5的indexOf的缺陷
在ES5中使用indexOf方法在数组中可以找到一个给定元素的第一个索引,如果不存在,则会返回-1
而且在查找数组的时候存在一定的缺陷:
- indexOf不能判断数组中是否包含NaN
- 也不能判断数组中是否包含空项
var arr1 = [,,,,,];
var arr2 = [null, undefined, NaN];
console.log(arr1[0], arr1[1])// undefined undefined
arr1.indexOf(undefined) // -1
arr2.indexOf(NaN); // -1
但是使用includes方法能够完美解决以上问题:
[,,,,,].includes(undefined) // true
[null, undefined, NaN].includes(NaN)] // true
14-3-2、includes的基本使用方法
includes方法用于查找一个数组中是否包含指定元素,并返回一个布尔值,如果包含此元素返回true,否则返回false
语法:
arr.includes(valueToFind[, fromIndex])
参数说明:
参数 | 描述 |
---|---|
valueToFind | 需要查找的目标值 |
fromIndex | (可选)从fromIndex索引处开始查找:如果传递负数则从 arr.length + fromIndex处开始查找;默认值为0 |
- 传递一个参数
会以该参数作为要查找的内容去数组中查找
const arr = ['ES5', 'ES6', 'ES10'];
console.log(arr.includes('ES6')); // true
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true
[undefined].includes(undefined) // true
- 不传递参数
与字符串的includes方法一样,如果不传递参数,那么会把第一个参数设置为undefined
- 但是要注意的是这里的undefined不是字符串的"undefined"
[undefined].includes(); // true
['undefined'].includes(); // false
- 传递两个参数
当传递两个参数时,第二个参数表示搜索的起始位置:
- 当第二个参数大于等于数组长度时,会返回false
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, 2); // true
- 当第二个参数为负数时会从 arr.length + 第二个参数的位置开始搜索,但如果此时还是负数的话会被当做0来搜索整个数组
const arr = ['a', 'b', 'c'];
arr.includes('a', -10); // true
arr.includes('b', -10); // true
arr.includes('a', -2); // false
总结:
- includes返回的是布尔值更容易做逻辑判断
- indexOf不能对数组空项和数组项为NaN的元素进行查找,而includes可以很好的解决此问题
14-4、find & findIndex
我们知道了includes方法用于查找数组中是否有符合条件的元素,但是我们有时候想得符合条件的元素本身或者符合条件的元素索引,而ES5中的filter方法虽然可以过滤出来符合条件的元素但返回的是一个数组,我们需要进一步取出来里面的元素。而ES6提供的find和findIndex方法可以分别用于查找出符合条件的元素、符合条件的元素的索引:
- find方法用于查找符合条件的第一个元素
- findIndex方法用与查找符合条件的第一个元素的索引
find方法与findIndex方法都只是关注第一个查找到的结果,在查找到结果以后就不会继续查找了
语法:
arr.find(callback[, thisArg])
arr.findIndex(callback[, thisArg])
参数说明:
参数 | 描述 |
---|---|
callback | 回调函数,接收数组中的每一项来执行其中的逻辑,当返回true时终止调用 |
thisArg | (可选)用于绑定执行callback时的this指向 |
- callback函数有三个参数:当前元素值、当前元素索引、以及数组本身;数组中的每一项元素都会执行一次callback函数直至callback函数返回true,并将查找的结果返回;如果所有数组元素执行完毕后均未返回true则将返回undefined
- 如果提供了thisArg参数,那么它将作为每次callback函数执行时的this,如果未提供,则使用undefined
14-4-1、对比ES5中的filter
const arr = [1,2,3,4,5]
const find = arr.filter(function(item) {
return item % 2 === 0
})
console.log(find) // [2, 4]
可见filter会将所有符合条件的元素放入一个数组中返回
14-4-2、find & findIndex的基本使用方法
const arr = [1, 4, 3, 6, 5]
const target1 = arr.find(function(item) {
return item % 2 === 0
})
console.log(target1) // 4
const target2 = arr.findIndex(function(item) {
return item % 2 === 0
})
console.log(target) // 1
总结:
- find返回数组中符合条件的第一个值
- findIndex返回数组中符合条件的第一个值的所以
- filter方法返回满足条件的所有元素组成的新数组
14-5、copyWithin
ES5中没有对数组内元素的复制和替换,如果要实现数组内的替换需要针对性的操作。ES6提供的copyWithin方法可以实现数组内元素的复制而别不会改变数组长度。
- 注意!不会返回一个新数组,而是改变原数组
语法:
arr.copyWithin(target[, start[, end]])
参数说明:
参数 | 描述 |
---|---|
target | 开始替换数据的起始位置,复制元素到该位置,如果是负数则会将target从末尾开始计算 |
start | (可选)开始复制的起始位置,如果是负数则将start从末尾开始计算,如果不传递则会从0开始 |
end | (可选)开始复制的结束位置,不包括该位置,如果是负数,end则将从末尾开始计算 |
14-5-1、copyWithin的基本使用方法
- 传递一个参数
- 当第一个参数是0或者不传递时,则会复制整个数组,并从起始位置开始替换复制的数据
const arr = [1, 2, 3, 4, 5];
arr.copyWithin(0); // [1, 2, 3, 4, 5]
arr.copyWithin(); // [1, 2, 3, 4, 5]
(当地一个参数为0时,会从第一个位置的元素开始复制一整个数组,然后替换整个数组,所以数组还是[1,2,3,4,5];当不传递时,默认第一个参数为0)
- 当第一个参数大于0时,则复制整个数组并从指定位置开始替换,但如果参数大于等于数组长度时则返回原数组
[1, 2, 3, 4, 5].copyWithin(2); // [1, 2, 1, 2, 3]
[1, 2, 3, 4, 5].copyWithin(5); // [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5].copyWithin(6); // [1, 2, 3, 4, 5]
- 当第一个参数小于0时,则从末尾开始计算需要替换的位置来替换为复制的数据
[1, 2, 3, 4, 5].copyWithin(-2); // [1, 2, 3, 1, 2]
[1, 2, 3, 4, 5].copyWithin(-5); // [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5].copyWithin(-6); // [1, 2, 3, 4, 5]
- 传递两个参数
第二个参数表示开始复制数组的起始位置,因为没有第三个参数,所以复制的结束位置时数组中的结尾
- 当第二个参数大于0时
const arr = [1, 2, 3, 4, 5];
arr.copyWithin(0, 3); // [4, 5, 3, 4, 5]
(这里表示从索引为3的位置开始复制复制到数组的结尾,即:复制 4,5;然后从索引为0的位置开始替换,所以最后结果为[ 4 ,5 ,3 ,4 ,5 ])
- 当第二个参数小于0时
const arr = [1, 2, 3, 4, 5];
arr.copyWithin(0, -3); // [3, 4, 5, 4, 5]
(此处表示从倒数第三个的位置开始复制元素,复制到数组结尾,所以复制的为3,4,5;然后从数组第0项的位置开始替换)
- 传递三个参数
当传递第三个参数的时候,该参数表示复制元素的结束位置,但是不包含该位置
- 当第三个参数大于0时
const arr = [1, 2, 3, 4, 5];
arr.copyWithin(1, 3, 4); // [1, 4, 3, 4, 5]
- 当第三个参数小于0时,会从末尾开始计算
[1,2,3,4,5].copyWithin(1, 0, -2); // [1,1,2,3,5]
(此处表示从索引位置为0的位置复制到倒数第二位,但要注意不包含倒数第二位数据,所以 4 不会被复制进去,复制的内容是:1,2,3,所以结果为[1,1,2,3,5])
而且要注意的是,如果负数表示的倒数位置在第二个参数之前,那么将会没有数据被复制,会返回原数组:
[1, 2, 3, 4, 5].copyWithin(1, 3, -4);// [1, 2, 3, 4, 5]
14-6、fill
在定义一个数组的时候我们有时需要给数组设置默认值进行填充,ES6便提供了fill方法:
- 此方法类似于copyWithin,都是替换数组中的值
- 而fill可以替换一个指定的值,copyWithin则是复制数组中的值去替换指定位置的值,不能指定值
语法:
arr.fill(value[, start[, end]])
参数描述:
参数 | 描述 |
---|---|
value | 用来填充数组元素的值 |
start | (可选)起始的索引,默认值是0,如果是负数,则从末尾开始计算 |
end | (可选)结束的索引,默认是数组的长度,如果是负数,则从末尾开始计算 |
14-6-1、fill的基本使用方法
- 初始化一个默认值的数组
const arr = new Array(5).fill(1)
console.log(arr) // [1, 1, 1, 1, 1]
- 传递两个参数时
- 当传递两个参数时,第二个参数表示从数组的哪个位置开始填充(包括该位置)
- 如果为负数,则表示从倒数第几项开始填充(包括该位置)
- 如果负数表示的倒数的位置超出了数组范围,则会从索引为0的位置开始填充
[1, 2, 3].fill(0, 0); // [0, 0, 0]
[1, 2, 3].fill(0, 1); // [1, 0, 0]
[1, 2, 3].fill(0, -1); // [1, 2, 0]
[1, 2, 3].fill(0, -3); // [0, 0, 0]
[1, 2, 3].fill(0, -5); // [0, 0, 0]
- 传递三个参数时
- 传递三个参数时,第三个参数表示替换结束的位置(不包含该位置)
- 如果第三个参数传递的是负数,则从末尾开始计算(不包含该位置)
- 如果第二个参数与第三个参数表示的位置相同或结束位置在开始位置之前,则不会进行填充
['a', 'b', 'c'].fill(4, 1, 2); // ["a", 4, "c"]
['a', 'b', 'c'].fill(4, -3, -2); // [4, "b", "c"]
['a', 'b', 'c'].fill(4, -3, 1); // [4, "b", "c"]
['a', 'b', 'c'].fill(4, -2, 1); // ["a", "b", "c"]
['a', 'b', 'c'].fill(4, -2, 0); // ["a", "b", "c"]
14-6-2、fill方法的注意事项
需要注意的是,如果fill方法填充的是对象,由于对象是引用类型,那么更改其中某一项的内容,其他项内容也会跟着改变:
const arr = Array(3).fill({}) // [{}, {}, {}]
arr[1].name = 'zs'; // [{name: 'zs'}, {name: 'zs'}, {name: 'zs'}]
14-7、flat
在开发中我们有时会遇到多维数组的拍平处理,也就是对数组进行扁平化。在ES5中没有方法能处理这样的需求,所以大部分会借助函数库来实现。
14-7-1、ES5中实现数组扁平化
ES5中我们可以采用递归的方式去处理:
function flat(arr){
if(Object.prototype.toString.call(arr) != "[object Array]"){return false};
let res = [];
for(var i=0;i<arr.length;i++){
if(arr[i] instanceof Array){
res = res.concat(flat(arr[i]))
}else{
res.push(arr[i])
}
}
return res;
};
const arr = [1,2,[3,4,5,[6,7,8],9],10,[11,12]];
flat(arr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
14-7-2、flat的基本使用方法
flat方法接收一个参数,该参数为数值类型,表示处理扁平化数组的深度,生成后的新数组是独立存在的,不会对原数组产生影响,所以我们也可以使用它来深拷贝一个数组。
语法:
var newArray = arr.flat([depth])
参数描述:
参数 | 描述 |
---|---|
depth | 指定要提取嵌套数组的深度,默认为1 |
const arr = [1, 2, [3, 4, [5, 6]]];
arr.flat(); // [1, 2, 3, 4, [5, 6]]
arr.flat(2); // [1, 2, 3, 4, 5, 6]
- 小技巧:我们可以使用Infinity来展开任意深度的嵌套数组
const arr = [1, 2, [3, 4, [5, 6, [7, 8]]]];
arr.flat(Infinity); // [1, 2, 3, 4, 5, 6, 7, 8]
- 如果数组包含空项时,使用flat会将数组中的空项进行移除
const arr = [1, 2, , 4, 5];
arr.flat(); // [1, 2, 4, 5]
14-7-3、使用flat深拷贝数组
由于flat方法拍平后的数组不会影响原数组,所以我们可以传入参数0来不让其扁平化,进而达到深拷贝的效果:
const arr1 = [1, 2, [3, 4, [5, 6]]];
const arr2 = arr1.flat(0);
arr2.push(7)
console.log(arr1) // [1, 2, [3, 4, [5, 6]]]
console.log(arr2) // [1, 2, [3, 4, [5, 6]], 7]
14-8、Array.isArray
在ES5中没有能严格判断JS对象是否为数组的方法,都会存在一定的问题,比较受大家认可的是借助toString来进行判断,但是这种方式不是很简洁。ES6中提供了Array.isArray方法可以更加简洁的判断一个JS对象是否为数组。
14-8-1、ES5中判断数组的方法
ES5中可以通过instanceOf、constructor去判断,但这两种方式都存在弊端,我们来一一看一下:
14-8-1-1、通过instanceOf判断
14-8-1-1-1、instanceOf的基本使用方法
instanceOf运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链,判断方式如下:
const arr = ['a', 'b', 'c'];
console.log(arr instanceof Array); // true
- 数组实例的原型链指向的是Array.prototype属性,instanceof运算符就是用来检测Array.prototype属性是否存在于数组的原型链上
14-8-1-1-2、instanceOf的弊端
- prototype属性是可以修改的,所以并不是最初判断为true就一定永远为真
- 在网站中,脚本可以拥有多个全局环境,比如在html中拥有多个iframe对象,instanceof的验证结果可能不会符合预期:
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = window.frames[0].Array;
const arr = new iframeArray('a', 'b', 'c');
console.log(arr instanceof Array); // false
console.log(arr) // ["a", "b", "c"]
我们可以打开控制台,输入上面的代码,现在body上创建并添加一个iframe对象,并把它插入到当前的网页中。这时我们可以获取 iframe 中数组构造函数。通过这个构造函数去实例化一个数组,这时再用 instanceof 去判断就会返回 false,但是案例中的 arr 确实是一个数组,这就是 instanceof 判断数组所带来的问题。
14-8-1-2、通过constructor判断
Array是JS中的内置构造函数,构造函数属性prototype中的constructor指向构造函数,所以可以通过constructor属性判断是否为一个数组
14-8-1-2-1、constructor判断基本使用方法
const arr = new Array('a', 'b', 'c');
arr.constructor === Array; //true
14-8-1-2-2、constructor判断的弊端
由于constructor是可以被重写的,所以不能确保一定是数组,如下:
const str = 'abc';
str.constructor = Array;
str.constructor === Array // true
- str显然不是数组,但是可以把constructor指向Array构造函数,这样再去判断就是有问题的了
14-8-2、Array.isArray的使用方法
语法:
Array.isArray(obj)
参数描述:
obj表示需要检测的JS对象
// 下面的函数调用都返回 true
Array.isArray([]);
Array.isArray([10]);
Array.isArray(new Array());
Array.isArray(new Array('a', 'b', 'c'))
// 鲜为人知的事实:其实 Array.prototype 也是一个数组。
Array.isArray(Array.prototype);
// 下面的函数调用都返回 false
Array.isArray();
Array.isArray({});
Array.isArray(null);
Array.isArray(undefined);
Array.isArray(17);
Array.isArray('Array');
Array.isArray(true);
Array.isArray(false);
Array.isArray(new Uint8Array(32))
Array.isArray({ __proto__: Array.prototype });
14-8-3、自定义isArray(toString的方式)
在ES5中比较通用的方法是使用Object.prototype.toString()去判断一个值的类型,也是各大主流库的标准。在不支持ES6语法的环境下,可以使用下面的方法为Array添上isArray方法:
if (!Array.isArray){
Array.isArray = function(arg){
return Object.prototype.toString.call(arg) === '[object Array]';
};
}