JavaScript数组所有API全解密

来源:互联网 发布:矢量软件哪个好 编辑:程序博客网 时间:2024/05/16 11:47

点击查看全文


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

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

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

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

Array构造器

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

// 使用Array构造器
vara = Array(8); // [undefined × 8]
// 使用对象字面量
varb = [];
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.

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

functionArray(){
  // 如果this不是Array的实例,那就重新new一个实例
  if(!(thisinstanceof arguments.callee)){
    returnnew 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的支持。

ChromeFirefoxEdgeSafari45+25+ 9.0+

即使其他版本浏览器不支持也不必担心,由于Array.of与Array构造器的这种高度相似性,实现一个polyfill十分简单。如下:

if(!Array.of){
  Array.of = function(){
    returnArray.prototype.slice.call(arguments);
  };
}

Array.from
语法:Array.from(arrayLike[, processingFn[, thisArg]])

Array.from的设计初衷是快速便捷的基于其他对象创建新数组,准确来说就是从一个类似数组的可迭代对象创建一个新的数组实例,说人话就是,只要一个对象有迭代器,Array.from就能把它变成一个数组(当然,是返回新的数组,不改变原对象)。

从语法上看,Array.from拥有3个形参,第一个为类似数组的对象,必选。第二个为加工函数,新生成的数组会经过该函数的加工再返回。第三个为this作用域,表示加工函数执行时this的值。后两个参数都是可选的。我们来看看用法。

varobj = {0: 'a', 1: 'b', 2:'c', length: 3};
Array.from(obj,function(value, index){
  console.log(value, index, this, arguments.length);
  returnvalue.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(newSet(['abc','def']));// ["abc", "def"]
// Map
Array.from(newMap([[1, 'abc'], [2, 'def']]));// [[1
,'abc'], [2, 'def']]
// 天生的类数组对象arguments
functionfn(){
  returnArray.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的支持。

ChromeFirefoxEdgeOperaSafari45+32+  9.0+

Array.isArray
顾名思义,Array.isArray用来判断一个变量是否数组类型。JS的弱类型机制导致判断变量类型是初级前端开发者面试时的必考题,一般我都会将其作为考察候选人第一题,然后基于此展开。在ES6提供该方法之前,ES5至少有如下5种方式判断一个值是否数组:

vara = [];
// 1.基于instanceof
ainstanceofArray;
// 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外,其它方法都不能正确判断变量的类型。

要知道,代码的运行环境十分复杂,一个变量可能使用浑身解数去迷惑它的创造者。且看:

vara = {
  __proto__: Array.prototype
};
// 分别在控制台试运行以下代码
// 1.基于instanceof
ainstanceofArray; // 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的支持。

ChromeFirefoxEdgeOperaSafari5+4+9+10.5+5+

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

if(!Array.isArray){
  Array.isArray = function(arg){
    returnObject.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 fori ina ifi == 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() 方法删除一个数组中的最后的一个元素,并且返回这个元素。如果是栈的话,这个过程就是栈顶弹出。

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

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

varo = {0:"cat", 1:"dog", 2:"cow", 3:"chicken", 4:"mouse", length:5}
varitem = 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。如下:

varo = {0:"cat", 1:"dog", 2:"cow", 3:"chicken", 4:"mouse"}
varitem = 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)

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

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

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

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

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

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

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

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

语法:arr.reverse()

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

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

varo = {0:"a", 1:"b", 2:"c", length:2};
varo2 = 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 看齐。如下:

varo = {0:"a", 1:"b", 2:"c",length:100};
varo2 = 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()

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

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

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

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

varo = {0:"a", 1:"b", 2:"c"};
varitem = 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"靠前。例如:

vararray = ["apple","Boy","Cat","dog"];
vararray2 = array.sort();
console.log(array);// ["Boy", "Cat", "apple", "dog"]
console.log(array2 == array); // true
 
array = [10, 1, 3, 20];
vararray3 = 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格式如下所示:

functioncompare(a, b){
  returna-b;
}

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

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

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

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

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

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

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

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

使用映射改善排序



点击查看全文


原创粉丝点击