【深度长文】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对象以外,拥有迭代器的对象还包括这些:StringSetMaparguments 等,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,和其他构造函数一样,你可以通过扩展 Arrayprototype 属性上的方法来给所有数组实例增加方法。

值得一说的是,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个,分别为poppushreverseshiftsortspliceunshift,以及两个ES6新增的方法copyWithinfill

对于能改变自身值的数组方法,日常开发中需要特别注意,尽量避免在循环遍历中去改变原数组的项。接下来,我们一起来深入地了解这些方法。

pop

posted @ 2020-08-12 17:29  yatolk  阅读(311)  评论(0编辑  收藏  举报