【深度长文】JavaScript数组所有API全解密
此文转载自louis blog
本文先占坑
,主要是想总结一下知识点,巩固自己的基础,代码都会自己本地先跑通,也会加上一些自己的理解和补充
数组是一种非常重要的数据类型,语法简单、灵活、高效。在多数编程语言中,数组都充当着至关重要的角色,以至于很难想象没有数组的编程语言会是什么模样。特别是JavaScript,
它天生的灵活性,又进一步发挥了数组的特长,丰富了数组的使用场景。可以豪不夸张地说,不深入地了解数组,不足以写JavaScript。
截止ES7规范,数组共包含33个标准的API方法和一个非标准的API方法,使用场景和使用方案纷繁复杂,其中有不少浅坑、深坑、甚至神坑。下面将从Array构造器及ES6新特性开始,逐步帮助你掌握数组。
Array构造函数
Array构造器用于创造一个新的数组。通常,我们推荐使用对象字面量创建数组,这是一个好习惯,但是总有对象字面量乏力的时候,
比如我想创建一个长度为8的空数组,请看下面两种方式
// 使用Array构造器
let a = Array(8); // [ <8 empty items> ]
// 使用对象字面量
let b = [];
b.length = 8; // [ <8 empty items> ]
Array构造器明显要简洁一些。
如上,我使用了Array(8)
,而不是new Array(8)
,这会有影响吗?实际上并没有影响,这得益于Array构造器内部对this指针的判断,规范内部做了如下处理
When Array is called as a function rather than as a constructor, it creates and initialises a new Array object. Thus the function call Array(…) is
equivalent to the object creation expression new Array(…) with the same arguments.
翻译一下
当将Array调用为函数而不是构造函数时,它将创建并初始化一个新的Array对象。 因此,函数调用Array(…)等效于具有相同参数的对象创建表达式new Array(…)。
function Array(){
// 如果this不是Array的实例,那就重新new一个实例
if(!(this instanceof arguments.callee)){
return new arguments.callee();
}
}
下面介绍Array构造函数的介绍,构造函数根据参数长度的不同,有如下两种不同的处理:
- new Array(arg1,arg2,...),参数长度为0时,返回空数组,长度大于等于2时,传入的参数将按照顺序依次成为新数组的第0-N项。
let a = Array(); // []
let b = Array(1,2,3) // []
- new Array(len),当len不是数值时,处理同上,返回一个只包含len元素一项的数组;当len为数值时,根据如下规范,len最大不能超过32位无符号整型,
即需要小于2的32次方(len最大为Math.pow(2,32)-1
或-1>>>0
),否则将抛出RangeError。
If the argument len is a Number and ToUint32(len) is equal to len, then the length property of the newly constructed object is set to ToUint32(len).
If the argument len is a Number and ToUint32(len) is not equal to len, a RangeError exception is thrown.
翻译一下:
如果参数len是Number并且ToUint32(len)等于len,则新构造对象的length属性将设置为ToUint32(len)。 如果参数len是Number并且ToUint32(len)不等于len,则会引发RangeError异常。
let a = Array('hello'); // ['hello']
let b = Array(3); // [<3 empty items>]
let c = Array(Math.pow(2,32)) // RangeError: Invalid array length
let d = Array(-1 >>> 0) // RangeError: Invalid array length
这里补充一个js右移>>>
的知识点,可以参考js >>> 0 谈谈 js 中的位运算
或者MDN按位操作符学习一下
以上,请注意Array构造器对于单个数值参数的特殊处理,如果仅仅需要使用数组包裹?若干参数,不妨试试使用Array.of
ES6新增的构造函数方法
鉴于数组的常用性,ES6专门扩展了数组构造器Array,新增了两个方法: Array.of
,Array.from
。下面看看怎么使用
Array.of
Array.of用于将参数依次转化为数组中的一项,然后返回这个新数组,而不管这个参数是数字还是其它。它基本上与Array构造器功能一致,唯一的区别就在单个数字参数的处理上。如下:
Array.of(8.0); // [8]
Array(8.0); // [empty × 8]
参数为多个,或单个参数不是数字时,Array.of 与 Array构造器等同。
Array.of(8.0, 5); // [8, 5]
Array(8.0, 5); // [8, 5]
Array.of('8'); // ["8"]
Array('8'); // ["8"]
因此,若是需要使用数组包裹元素,推荐优先使用Array.of方法。
目前,以下版本浏览器提供了对Array.of的支持。
Chrome | Firefox | Edge | Safari |
---|---|---|---|
45+ | 25+ | ✔️ | 9.0+ |
即使其他版本浏览器不支持也不必担心,由于Array.of与Array构造器的这种高度相似性,实现一个polyfill十分简单。如下:
if (!Array.of){
Array.of = function(){
return Array.prototype.slice.call(arguments);
};
}
Array.from
语法: Array.from(arrayLike[,processingFn[,thisArg]])
Array.from
的设计初衷是快速便捷的基于对象,准确的来说就是从一个类似数组的可迭代对象
创建一个新的数组实例,说人话就是,只要一个对象有迭代器,Array.from
就能把
它变成一个数组(当然是返回新的数组,不会改变原始值)
从语法上看,Array.from
拥有三个形参,第一个为类似数组的对象
,必选。第二个为加工函数
,新生成的数组会经过该函数的加工再返回。第三个为this
作用域,表示加工函数执行时
this
的值,后面两个参数都是可选的,下面我们来看下用法:
let obj = {0: 'a', 1: 'b', 2:'c', length: 3};
Array.from(obj, function(value, index){
console.log(value, index, this, arguments.length);
return value.repeat(3); //必须指定返回值,否则返回undefined
}, obj);
运行结果如下:
测试this参数
let obj = {0: 'a', 1: 'b', 2:'c', length: 3};
let obj1 = {0: 'f', 1: 'g', 2:'h', length: 3};
Array.from(obj, function(value, index){
console.log(value, index, this, arguments.length);
return value.repeat(3); //必须指定返回值,否则返回undefined
}, obj1);
结果如下;
可以看到加工函数的this作用被obj1取代,也可以看到加工函数默认有个形参,分别为迭代器当前的元素的值和其索引。
注意,一旦使用加工函数,必须明确指定返回值
,否则会隐式返回undefined
,最终生成的数组也会变成一个只包含若干个undefined
元素的空数组。
实际上,如果不需要指定this
指针,加工函数完全可以是一个箭头函数。上述代码可以简化成如下:
Array.from(obj, (value) => value.repeat(3))
除了上述obj
对象以外,拥有迭代器的对象还包括这些:String
,Set
,Map
,arguments
等,Array.from
统统可以处理。如下所示:
// String
Array.from('abd'); // ["a", "b", "d"]
// Set
Array.from(new Set(['abc', 'def'])); // ["abc", "def"]
// Map
Array.from(new Map([[1, 'abc'], [2, 'def']])); // [[1
, 'abc'], [2, 'def']]
// 天生的类数组对象arguments
function fn(){
return Array.from(arguments);
}
fn(1, 2, 3); // [1, 2, 3]
到这你可能以为Array.from就讲完了,实际上还有一个重要的扩展场景必须提下。比如说生成一个从0到指定数字的新数组,Array.from就可以轻易的做到。
Array.from({length: 10}, (v, i) => i); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
后面我们将会看到,利用数组的keys方法实现上述功能,可能还要简单一些。
Array.isArray
顾名思义,Array.isArray用来判断一个变量是否为数组类型。JS的弱类型机制导致判断变量类型是初级前端开发者面试时的必考题,ES5至少有如下5种方式
判断一个值是否为数组:
var a = [];
// 1.基于instanceof
a instanceof Array;
// 2.基于constructor
a.constructor === Array;
// 3.基于Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(a);
// 4.基于getPrototypeOf
Object.getPrototypeOf(a) === Array.prototype;
// 5.基于Object.prototype.toString
Object.prototype.toString.apply(a) === '[object Array]';
运行结果:
以上除了Object.prototype.toString
外,其他方法都不能正确的判断变量的类型。
要知道,代码的运行环境十分复杂,一个变量可能使用浑身解数去迷惑他的使用者。看下面的代码:
var b = {
__proto__: Array.prototype
};
// 分别在控制台试运行以下代码
// 1.基于instanceof
b instanceof Array; // true
// 2.基于constructor
b.constructor === Array; // true
// 3.基于Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(b); // true
// 4.基于getPrototypeOf
Object.getPrototypeOf(b) === Array.prototype; // true
运行结果:
以上,4种方法将全部返回true
,为什么呢?我们只是手动指定了某个对象的__proto__
属性为Array.prototype
,便导致了该对象继承了Array对象,这种毫不负责任的继承方式,
使得基于继承的判断方案瞬间土崩瓦解。
不仅如此,我们还知道,Array
是堆数据
,变量指向
的只是它的引用地址
,因此每个页面的Array
对象引用的地址都是不一样
的。iframe
中声明的数组,它的构造函数是iframe
中的Array
对象。
如果在iframe
声明了一个数组x
,将其赋值给父页面的变量y
,那么在父页面使用y instanceof Array
,结果一定是false
的。而最后一种返回的是字符串,不会存在引用问题。实际上,多页面或系统之间的交互只有字符串能够畅行无阻。
鉴于上述的两点原因,故笔者推荐使用最后一种方法去撩面试官(别提是我说的),如果你还不信,
这里恰好有篇文章跟我持有相同的观点:Determining with absolute accuracy whether or not a JavaScript object is an array。
回到ES6,使用Array.isArray则非常简单,如下:
Array.isArray([]); // true
Array.isArray({0: 'a', length: 1}); // false
实际上,是通过Object.prototype.toString
去判断一个值的类型,也是各大主流库的标准。因此Array.isArray
的polyfill通常如下:
if (!Array.isArray){
Array.isArray = function(arg){
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
数组推导
不是标准规范,暂不了解
原型
继承的常识告诉我们,js中所有的数组方法均来自于Array.prototype
,和其他构造函数一样,你可以通过扩展 Array
的 prototype
属性上的方法来给所有数组实例增加方法。
值得一说的是,Array.prototype
本身就是一个数组。
Array.isArray(Array.prototype); // true
console.log(Array.prototype.length);// 0
console.log([].__proto__.length);// 0
console.log([].__proto__);// [Symbol(Symbol.unscopables): Object]
方法
数组原型提供的方法非常之多,主要分为三种,一种是会改变自身值的,一种是不会改变自身值的,另外一种是遍历方法。
由于 Array.prototype 的某些属性被设置为[[DontEnum]],因此不能用一般的方法进行遍历,我们可以通过如下几种方式获取 Array.prototype 的所有方法:
Object.getOwnPropertyNames(Array.prototype);
console.dir(Array.prototype)
console.log(Array.prototype)
改变自身值的方法(9个)
基于ES6,改变自身值的方法一共有9个,分别为pop
、push
、reverse
、shift
、sort
、splice
、unshift
,以及两个ES6新增的方法copyWithin
和 fill
。
对于能改变自身值的数组方法,日常开发中需要特别注意,尽量避免在循环遍历中去改变原数组的项。接下来,我们一起来深入地了解这些方法。