JavaScript数组所有API全解密

点击查看全文


全文共13k+字,系统讲解了JavaScript数组的各种特性和API。

数组是一种非常重要的数据类型,它语法简单、灵活、高效。 在多数编程语言中,数组都充当着至关重要的角色,以至于很难想象没有数组的编程语言会是什么模样。特别是JavaScript,它天生的灵活性,又进一步发挥了数组的特长,丰富了数组的使用场景。可以豪不夸张地说,不深入地了解数组,不足以写JavaScript。

截止ES7规范,数组共包含33个标准的API方法和一个非标准的API方法,使用场景和使用方案纷繁复杂,其中有不少浅坑、深坑、甚至神坑。下面将从Array构造器及ES6新特性开始,逐步帮助你掌握数组。

声明:以下未特别标明的方法均为ES5已实现的方法。

Array构造器

Array构造器用于创建一个新的数组。通常,我们推荐使用对象字面量创建数组,这是一个好习惯,但是总有对象字面量乏力的时候,比如说,我想创建一个长度为8的空数组。请比较如下两种方式:

// 使用Array构造器
var a = Array(8); // [undefined × 8]
// 使用对象字面量
var b = [];
b.length = 8; // [undefined × 8]

Array构造器明显要简洁一些,当然你也许会说,对象字面量也不错啊,那么我保持沉默。
如上,我使用了Array(8)而不是new Array(8),这会有影响吗?实际上,并没有影响,这得益于Array构造器内部对this指针的判断,ELS5_HTML规范是这么说的:
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.

从规范来看,浏览器内部大致做了如下类似的实现:

function Array(){
   // 如果this不是Array的实例,那就重新new一个实例
   if (!( this instanceof arguments.callee)){
     return new arguments.callee();
   }
}

上面,我似乎跳过了对Array构造器语法的介绍,没事,接下来我补上。

Array构造器根据参数长度的不同,有如下两种不同的处理:

  • new Array(arg1, arg2,…),参数长度为0或长度大于等于2时,传入的参数将按照顺序依次成为新数组的第0至N项(参数长度为0时,返回空数组)。
  • 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.

以上,请注意Array构造器对于单个数值参数的特殊处理,如果仅仅需要使用数组包裹?? 若干参数,不妨使用Array.of,具体请移步下一节。


如果你想学习前端可以来这个群,首先是二九一,中间是八五一,最后是一八九,里面可以学习交流,也有资料可以下载。

ES6新增的构造函数方法

鉴于数组的常用性,ES6专门扩展了数组构造器Array ,新增2个方法:Array.of、Array.from。下面展开来聊。

Array.of
Array.of用于将参数依次转化为数组中的一项,然后返回这个新数组,而不管这个参数是数字还是其它。它基本上与Array构造器功能一致,唯一的区别就在单个数字参数的处理上。如下:

Array.of(8.0); // [8]
Array(8.0); // [undefined × 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拥有3个形参,第一个为类似数组的对象,必选。第二个为加工函数,新生成的数组会经过该函数的加工再返回。第三个为this作用域,表示加工函数执行时this的值。后两个参数都是可选的。我们来看看用法。

var 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);

执行结果如下:
jsapi1.png
可以看到加工函数的this作用域被obj对象取代,也可以看到加工函数默认拥有两个形参,分别为迭代器当前元素的值和其索引。

注意,一旦使用加工函数,必须明确指定返回值,否则将隐式返回undefined,最终生成的数组也会变成一个只包含若干个undefined元素的空数组。

实际上,如果不需要指定this,加工函数完全可以是一个箭头函数。上述代码可以简化如下:

Array.from(obj, (value) => value.repeat(3));

除了上述obj对象以外,拥有迭代器的对象还包括这些:String,Set,Map,arguments 等,Array.from统统可以处理。如下所示:

// String
Array.from( 'abc' ); // ["a", "b", "c"]
// 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.from的支持。

Chrome Firefox Edge Opera Safari
45+ 32+     9.0+

Array.isArray
顾名思义,Array.isArray用来判断一个变量是否数组类型。JS的弱类型机制导致判断变量类型是初级前端开发者面试时的必考题,一般我都会将其作为考察候选人第一题,然后基于此展开。在ES6提供该方法之前,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 a = {
   __proto__: Array.prototype
};
// 分别在控制台试运行以下代码
// 1.基于instanceof
a instanceof Array; // true
// 2.基于constructor
a.constructor === Array; // true
// 3.基于Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(a); // true
// 4.基于getPrototypeOf
Object.getPrototypeOf(a) === Array.prototype; // true

以上,4种方法将全部返回true,为什么呢?我们只是手动指定了某个对象的__proto__属性为Array.prototype,便导致了该对象继承了Array对象,这种毫不负责任的继承方式,使得基于继承的判断方案瞬间土崩瓦解。

不仅如此,我们还知道,Array是堆数据,变量指向的只是它的引用地址,因此每个页面的Array对象引用的地址都是不一样的。iframe中声明的数组,它的构造函数是iframe中的Array对象。如果在iframe声明了一个数组x,将其赋值给父页面的变量y,那么在父页面使用y instanceof Array ,结果一定是false的。而最后一种返回的是字符串,不会存在引用问题。实际上,多页面或系统之间的交互只有字符串能够畅行无阻。

鉴于上述的两点原因,故笔者推荐使用最后一种方法去撩面试官(别提是我说的),如果你还不信,这里恰好有篇文章跟我持有相同的观点:

回到ES6,使用Array.isArray则非常简单,如下: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

目前,以下版本浏览器提供了对Array.isArray的支持。

Chrome Firefox Edge Opera Safari
5+ 4+ 9+ 10.5+ 5+

实际上,通过Object.prototype.toString去判断一个值的类型,也是各大主流库的标准。因此Array.isArray的polyfill通常长这样:

if (!Array.isArray){
   Array.isArray = function (arg){
     return Object.prototype.toString.call(arg) === '[object Array]' ;
   };
}

数组推导

ES6对数组的增强不止是体现在api上,还包括语法糖。比如说for of,它就是借鉴其它语言而成的语法糖,这种基于原数组使用for of生成新数组的语法糖,叫做数组推导。数组推导最初起早在ES6的草案中,但在第27版(2014年8月)中被移除,目前只有Firefox v30+支持,推导有风险,使用需谨慎。所幸如今这些语言都还支持推导:CoffeeScript、Python、Haskell、Clojure,我们可以从中一窥端倪。这里我们以python的for in推导打个比方:

# python for in 推导
a = [1, 2, 3, 4]
print [i * i for i in a if i == 3] # [9]

如下是SpiderMonkey引擎(Firefox)之前基于ES4规范实现的数组推导(与python的推导十分相似):

[i * i for (i of a)] // [1, 4, 9, 16]

ES6中数组有关的for of在ES4的基础上进一步演化,for关键字居首,in在中间,最后才是运算表达式。如下:

[ for (i of [1, 2, 3, 4]) i * i] // [1, 4, 9, 16]

同python的示例,ES6中数组有关的for of也可以使用if语句:

// 单个if
[ for (i of [1, 2, 3, 4]) if (i == 3) i * i] // [9]
// 甚至是多个if
[ for (i of [1, 2, 3, 4]) if (i > 2) if (i < 4) i * i] // [9]

更为强大的是,ES6数组推导还允许多重for of。

[ for (i of [1, 2, 3]) for (j of [10, 100]) i * j] // [10, 100, 20, 200, 30, 300]

甚至,数组推导还能够嵌入另一个数组推导中。

[ for (i of [1, 2, 3]) [ for (j of [10, 100]) i * j] ] // [[10, 100], [20, 200], [30, 300]]

对于上述两个表达式,前者和后者唯一的区别,就在于后者的第二个推导是先返回数组,然后与外部的推导再进行一次运算。

除了多个数组推导嵌套外,ES6的数组推导还会为每次迭代分配一个新的作用域(目前Firefox也没有为每次迭代创建新的作用域):

// ES6规范
[ for (x of [0, 1, 2]) () => x][0]() // 0
// Firefox运行
[ for (x of [0, 1, 2]) () => x][0]() // 2

通过上面的实例,我们看到使用数组推导来创建新数组比forEach,map,filter等遍历方法更加简洁,只是非常可惜,它不是标准规范。

ES6不仅新增了对Array构造器相关API,还新增了8个原型的方法。接下来我会在原型方法的介绍中穿插着ES6相关方法的讲解,请耐心往下读。

原型

继承的常识告诉我们,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]

有关Symbol(Symbol.unscopables)的知识,这里不做详述,具体请移步后续章节。

方法

数组原型提供的方法非常之多,主要分为三种,一种是会改变自身值的,一种是不会改变自身值的,另外一种是遍历方法。

由于 Array.prototype 的某些属性被设置为[[DontEnum]],因此不能用一般的方法进行遍历,我们可以通过如下方式获取 Array.prototype 的所有方法:

Object.getOwnPropertyNames(Array.prototype); // ["length", "constructor", "toString", "toLocaleString", "join", "pop", "push", "reverse", "shift", "unshift", "slice", "splice", "sort", "filter", "forEach", "some", "every", "map", "indexOf", "lastIndexOf", "reduce", "reduceRight", "copyWithin", "find", "findIndex", "fill", "includes", "entries", "keys", "concat"]

改变自身值的方法(9个)
基于ES6,改变自身值的方法一共有9个,分别为pop、push、reverse、shift、sort、splice、unshift,以及两个ES6新增的方法copyWithin 和 fill。

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

pop
pop() 方法删除一个数组中的最后的一个元素,并且返回这个元素。如果是栈的话,这个过程就是栈顶弹出。

var array = [ "cat" , "dog" , "cow" , "chicken" , "mouse" ];
var item = array.pop();
console.log(array); // ["cat", "dog", "cow", "chicken"]
console.log(item); // mouse

由于设计上的巧妙,pop方法可以应用在类数组对象上,即 鸭式辨型. 如下:

var o = {0: "cat" , 1: "dog" , 2: "cow" , 3: "chicken" , 4: "mouse" , length:5}
var item = Array.prototype.pop.call(o);
console.log(o); // Object {0: "cat", 1: "dog", 2: "cow", 3: "chicken", length: 4}
console.log(item); // mouse

但如果类数组对象不具有length属性,那么该对象将被创建length属性,length值为0。如下:

var o = {0: "cat" , 1: "dog" , 2: "cow" , 3: "chicken" , 4: "mouse" }
var item = Array.prototype.pop.call(o);
console.log(array); // Object {0: "cat", 1: "dog", 2: "cow", 3: "chicken", 4: "mouse", length: 0}
console.log(item); // undefined

push
push()方法添加一个或者多个元素到数组末尾,并且返回数组新的长度。如果是栈的话,这个过程就是栈顶压入。

语法:arr.push(element1, ..., elementN)

var array = [ "football" , "basketball" , "volleyball" , "Table tennis" , "badminton" ];
var i = array.push( "golfball" );
console.log(array); // ["football", "basketball", "volleyball", "Table tennis", "badminton", "golfball"]
console.log(i); // 6

同pop方法一样,push方法也可以应用到类数组对象上,如果length不能被转成一个数值或者不存在length属性时,则插入的元素索引为0,且length属性不存在时,将会创建它。

var o = {0: "football" , 1: "basketball" };
var i = Array.prototype.push.call(o, "golfball" );
console.log(o); // Object {0: "golfball", 1: "basketball", length: 1}
console.log(i); // 1

实际上,push方法是根据length属性来决定从哪里开始插入给定的值。

var o = {0: "football" , 1: "basketball" ,length:1};
var i = Array.prototype.push.call(o, "golfball" );
console.log(o); // Object {0: "football", 1: "golfball", length: 2}
console.log(i); // 2

利用push根据length属性插入元素这个特点,可以实现数组的合并,如下:

var array = [ "football" , "basketball" ];
var array2 = [ "volleyball" , "golfball" ];
var i = Array.prototype.push.apply(array,array2);
console.log(array); // ["football", "basketball", "volleyball", "golfball"]
console.log(i); // 4

reverse
reverse()方法颠倒数组中元素的位置,第一个会成为最后一个,最后一个会成为第一个,该方法返回对数组的引用。

语法:arr.reverse()

var array = [1,2,3,4,5];
var array2 = array.reverse();
console.log(array); // [5,4,3,2,1]
console.log(array2===array); // true

同上,reverse 也是鸭式辨型的受益者,颠倒元素的范围受length属性制约。如下:

var o = {0: "a" , 1: "b" , 2: "c" , length:2};
var o2 = Array.prototype.reverse.call(o);
console.log(o); // Object {0: "b", 1: "a", 2: "c", length: 2}
console.log(o === o2); // true

如果 length 属性小于2 或者 length 属性不为数值,那么原类数组对象将没有变化。即使 length 属性不存在,该对象也不会去创建 length 属性。特别的是,当 length 属性较大时,类数组对象的『索引』会尽可能的向 length 看齐。如下:

var o = {0: "a" , 1: "b" , 2: "c" ,length:100};
var o2 = Array.prototype.reverse.call(o);
console.log(o); // Object {97: "c", 98: "b", 99: "a", length: 100}
console.log(o === o2); // true

shift
shift()方法删除数组的第一个元素,并返回这个元素。

语法:arr.shift()

var array = [1,2,3,4,5];
var item = array.shift();
console.log(array); // [2,3,4,5]
console.log(item); // 1

同样受益于鸭式辨型,对于类数组对象,shift仍然能够处理。如下:

var o = {0: "a" , 1: "b" , 2: "c" , length:3};
var item = Array.prototype.shift.call(o);
console.log(o); // Object {0: "b", 1: "c", length: 2}
console.log(item); // a

如果类数组对象length属性不存在,将添加length属性,并初始化为0。如下:

var o = {0: "a" , 1: "b" , 2: "c" };
var item = Array.prototype.shift.call(o);
console.log(o); // Object {0: "a", 1: "b", 2:"c" length: 0}
console.log(item); // undefined

sort
sort()方法对数组元素进行排序,并返回这个数组。sort方法比较复杂,这里我将多花些篇幅来讲这块。

语法:arr.sort([comparefn])

comparefn是可选的,如果省略,数组元素将按照各自转换为字符串的Unicode(万国码)位点顺序排序,例如"Boy"将排到"apple"之前。当对数字排序的时候,25将会排到8之前,因为转换为字符串后,"25"将比"8"靠前。例如:

var array = [ "apple" , "Boy" , "Cat" , "dog" ];
var array2 = array.sort();
console.log(array); // ["Boy", "Cat", "apple", "dog"]
console.log(array2 == array); // true
 
array = [10, 1, 3, 20];
var array3 = array.sort();
console.log(array3); // [1, 10, 20, 3]

如果指明了comparefn,数组将按照调用该函数的返回值来排序。若 a 和 b 是两个将要比较的元素:

  • 若 comparefn(a, b) < 0,那么a 将排到 b 前面;
  • 若 comparefn(a, b) = 0,那么a 和 b 相对位置不变;
  • 若 comparefn(a, b) > 0,那么a , b 将调换位置;

如果数组元素为数字,则排序函数comparefn格式如下所示:

function compare(a, b){
   return a-b;
}

如果数组元素为非ASCII字符的字符串(如包含类似 e、é、è、a、? 或中文字符等非英文字符的字符串),则需要使用String.localeCompare。下面这个函数将排到正确的顺序。

var array = [ '互' , '联' , '网' , '改' , '变' , '世' , '界' ];
var array2 = array.sort();
 
var array = [ '互' , '联' , '网' , '改' , '变' , '世' , '界' ]; // 重新赋值,避免干扰array2
var array3 = array.sort( function (a, b) {
   return a.localeCompare(b);
});
 
console.log(array2); // ["世", "互", "变", "改", "界", "网", "联"]
console.log(array3); // ["变", "改", "互", "界", "联", "世", "网"]

如上,『互联网改变世界』这个数组,sort函数默认按照数组元素unicode字符串形式进行排序,然而实际上,我们期望的是按照拼音先后顺序进行排序,显然String.localeCompare 帮助我们达到了这个目的。

为什么上面测试中需要重新给array赋值呢,这是因为sort每次排序时改变的是数组本身,并且返回数组引用。如果不这么做,经过连续两次排序后,array2 和 array3 将指向同一个数组,最终影响我们测试。array重新赋值后就断开了对原数组的引用。

同上,sort一样受益于鸭式辨型,比如:

var o = {0: '互' ,1: '联' ,2: '网' ,3: '改' ,4: '变' ,5: '世' ,6: '界' ,length:7};
Array.prototype.sort.call(o, function (a, b){
   return a.localeCompare(b);
});
console.log(o); // Object {0: "变", 1: "改", 2: "互", 3: "界", 4: "联", 5: "世", 6: "网", length: 7}, 可见同上述排序结果一致

注意:使用sort的鸭式辨型特性时,若类数组对象不具有length属性,它并不会进行排序,也不会为其添加length属性。

var o = {0: '互' ,1: '联' ,2: '网' ,3: '改' ,4: '变' ,5: '世' ,6: '界' };
Array.prototype.sort.call(o, function (a, b){
   return a.localeCompare(b);
});
console.log(o); // Object {0: "互", 1: "联", 2: "网", 3: "改", 4: "变", 5: "世", 6: "界"}, 可见并未添加length属性

使用映射改善排序



点击查看全文


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值