本系列内容由ZouStrong整理收录
整理自《JavaScript权威指南(第六版)》,《JavaScript高级程序设计(第三版)》
数组在所有语言中都是数据的有序集合,每个数据叫做一个元素,元素的位置已基于0的数字表示,称为索引
但JavaScript中的数组有着很大的不同
Array类型继承自Object.prototype,是Object的实例
Array instancrof Object //true
而数组对象又是Array类型的实例(所以数组对象更是Object的实例)
var a=[1,2,3];
a instanceof Array //true
a instanceof Object //true
可以通过数组直接量、关键字new来创建数组
创建数组最简单的方式就是使用数组直接量,在方括号中将数组元素用逗号隔开即可
var emp=[];
var arr=[1,2,3,true,'strong'];
数组中的直接量不一定要是常量,可以是任意的表达式
var a = 1;
var arr=[a,a+1,a+2];
也可以包含另一个数组直接量或者对象直接量
var arr=[ [1,{x:2}] , [1,2] ];
如果省略数组直接量中的某个值,会创建稀疏数组,省略的位置没有值(访问该位置会返回undefined)
var arr=[1 , , 3]; //[1 , , 3]
最后一项后面不要添加逗号,否则会在IE8及以下版本中导致数组长度计算异常
var person = [1 , 2 , 3 , ]; //length为3或者4
与对象一样,在使用数组字面量语法时,也不会调用Array构造函数
new运算符后跟Array构造函数
var arr= new Array(); //空数组 [](length为0)
还可以在创建数组时,指定数组的长度(这里说的指定长度,只是预分配空间,前面也说过,数组是动态的,因此数组的长度不会受这个限制)
var arr= new Array(3); // 长度为3的空数组(每一项都没有值)
更可以在创建数组时,指定数组包含的元素
var arr= new Array("3"); //包含一个元素3的数组——['3']
var arr= new Array(1,2,3); //包含指定元素的数组(length由参数个数决定)
注:new和括号可以省略其中一个,但不可同时省略
关于创建Object实例和Array实例时,省略new或者括号的问题,我想说的是管它能不能同时省略,自己写的时候始终不省略,而看到省略写法时,不要觉得奇怪就好
访问和设置数组元素都是用方括号([])运算符,方括号里是一个返回非负整数的任意表达式(基于0)
var a =[1,2,3];
a[0]; //1
a[3]=4; //[1,2,3,4]
var i=4;
a[i]=5; //[1,2,3,4,5]
数组是对象的特殊形式,使用方括号访问数组元素就像用方括号访问对象的属性一样,JavaScript将指定的数字索引转换成字符串,然后将其作为属性名来使用
a={};
a[1]="one";
只不过,数组的特别之处在于,当使用非负整数作为属性名时,数组会自动维护其length属性值
var person = [1,2,"strong","good"];
var name = person[2] ; //访问第3项
person[3] = 4; //修改第4项
person[99] = 100; //新增第100项
person.length; //100
注:设置数组项时,如果索引超出了当前数组范围,则数组长度自动增加到该索引加1
清楚的区分数组的索引和对象的属性名是非常有用的
可以使用负数或非整数来索引数组,在这种情况下,数值转换为字符串,字符串作为属性名来使用,并且只能作为常规的对象属性,而非数组的索引
var arr = [];
arr.name = 'strong';
arr[-1] ='zou';
arr[1.1] = 'hello';
arr.length = 0
arr.name; //'strong' 或者 arr['name']
arr[-1]; //'zou'
arr[1.1]; //'hello'
但是如果使用的是非负整数的字符串,他会被当做索引,而非对象属性,使用一个浮点数和一个整数相等时的情况也是这样
var arr =[];
arr["2"]=3;
arr.length //3
arr[4.0]=100;
arr.length //5
因为数组索引仅仅是对象属性名的一种特殊形式,所以JavaScript数组没有“越界”错误的概念,当试图查询不存在的索引(属性)时,不会报错,而是返回undefined,这一点与普通对象一致
a=[1,2]
a[1] //2
a[2] //undefined
a[-1] //undefined
数组是对象,因此可以从原型中继承元素,也可以使用getter和setter设置元素,只不过访问这种数组元素的时间与常规对象的时间就相同了
JavaScript不支持真正的多维数组,但是可以用数组的数组来近似,访问数组的数组中的元素,只要使用两次[]即可
var arr=[1,2,[3,4]];
arr[2][1] //4
如何检测某个对象是不是数组?显然typeof操作符不行(对除了函数以外的所有对象都是返回"Object")
对于同一个网页或者一个全局作用域而言,使用instanceof 运算符即可
ary instanceof Array //返回true 或 false
如果网页中包含多个框架或者窗体,那实际上就存在两个以上不同的全局执行环境(window),从而存在两个以上不同版本的Array构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数
该方法判断某个值到底是不是数组,而不管它是在哪个全局执行环境中创建的
Array.isArray( ary ) //返回true 或 false,IE8及以下不支持
arr.constructor === Array //true或false
最根本的方法就是检测数组的类特性——"Array"
Object.prototype.toString.call(arr).slice(8,-1); //"Array"
因此封装一个isArray()函数,可以这样
function isArray(arr){
if(Array.isArray){
return Array.isArray(arr);
}else{
return Object.prototype.toString.call(arr).slice(8,-1)=="Array";
}
}
稀疏数组就是包含从零开始的不连续索引的数组
通常数组的length属性代表数组中元素的个数,但如果数组是稀疏的,length属性则大于元素的个数
可以使用Array()构造函数或者简单的指定数组的索引值大于当前数组的长度来创建稀疏数组
var arr = new Array(10); //没有元素,但是length为10
2 in arr; //false,可以看到确实没有值,尽管访问的时候返回undefined
var arr =[];
arr[99]=0; //只有一个元素,但是length为100
使用delete运算符也可以产生稀疏数组
var arr=[1,2,3];
delete arr[2];
2 in arr; //false
arr.length //仍然是3,delete不会改变数组长度
当在数组直接量中省略值时也会创建稀疏数组
var arr = [1,,3];
1 in arr; // false,索引1处没有元素
数组继承的constructor属性返回数组的构造函数
var arr = [];
arr.constructor === Array; //true
数组的length属性返回数组的长度(这个属性使其区别于常规的对象)
对于非稀疏数组
length === 元素的个数 === 最后一项的索引+1
对于稀疏数组
length > 元素的个数
length >= 最后一项的索引+1
通过设置length属性,可以从数组的末尾移除项或向数组中添加新项
最快的删除数组中元素的方法,就是
arr.length = 0; //删除数组中所有的元素
利用length属性可以方便地在数组末尾添加新项
var colors = ["red", "blue", "green"]; // 创建一个包含3个字符串的数组
colors[colors.length] = "black"; //(在位置3)添加元素
colors[colors.length] = "brown"; //(在位置4)再添加元素
在ECMAScript5中,如果将数组的length属性设置为不可写的,那就不能够通过length添加或者删除元素了,但是可以使用delete删除元素,因为delete不影响length
Object.defineProperty(arr,'length',{writable:false})
类似的,如果让一个数组元素不可配置,就不能删除该元素了,那么length属性也不能设置为小于该元素的索引了
所有对象都具有继承自Object.prototype的toString()、toLocaleString()、和valueOf()等方法,数组也不例外,并且进行了重写
数组的valueOf()方法返回原数组(两者指向同一个数组)
返回数组中每个元素拼接而成的以逗号分隔的字符串,原数组不变(在有必要的情况下,会调用数组每一项上的的toString()方法)
var a =[1,2,3];
a.toString(); //"1,2,3"
var a =[1,2,[3,4]];
a.toString(); //"1,2,3,4"
var a =[1,2,{x:1}];
a.toString(); //"1,2,[object Object]"
所有在希望使用字符串的地方使用了数组时都会隐式调用toString()方法,例如
alert([1,2,3]); //"1,2,3",隐式调用了toString()
[1,2]+"s"; //"1,2s"
作为toString()的本地实现,两者在数组上没有区别,都是返回数组中每个元素拼接而成的以逗号分隔的字符串,原数组不变(在有必要的情况下,会调用数组每一项的toLocaleString()方法)
继承自Object.prototype的方法还有hasOwnProperty()、propertyIsEnumerable()、isPrototypeOf(),不再赘述,除了这些方法,后续的数组特有方法继承自Array.prototype(或者Array?)
join()方法更加灵活,可使用不同的分隔符来创建字符串
只接收一个可选参数,即用做分隔符的字符串(默认逗号分隔,跟toString()一样),返回创建的字符串,原数组不变
var array=[1,2,3];
array.join() //"1,2,3"
array.join(",") // "1,2,3"
array.join("") //"123"
array.join(" ") //"1 2 3"
在使用上面三种方式创建字符串时,数组中的undefined项或者null项,在创建的字符串中,以空字符串("")表示
[1,null,3].join(); // "1,,3"
var array = new Array(5);
array.join("&"); //"&&&&"
join()方法是字符串方法split()的反向操作,后者用于根据指定字符将字符串打散成数组
var str="1,2,3";
var arr1 = str.split(","); //["1","2","3"]
var arr2 = str.split(""); //["1", ",", "2", ",", "3"]
最简单的添加数组元素的方法就是为新索引赋值
var arr=[];
arr[0]="one"; //["one"]
push()方法,在数组的末尾增加一个或者多个元素
接收任意数量的参数,将参数顺序不变添加到数组末尾,返回修改后数组的长度,原数组改变
var arr=[];
arr.push("one"); // ["one"]
arr.push("two","thr"); // ["one","two","thr"]
//注意这里,不是是拼接数组哦
arr.push(["two"]); // ["one","two","thr",["two"]]
unshift()方法,在数组的头部增加一个或者多个元素
接收任意数量的参数,将参数顺序不变(参数是一次性插入的)添加到数组前端,返回修改后数组的长度,原数组改变
var arr=[];
arr.unshift("one"); // ["one"]
arr.unshift("two","thr"); // ["two","thr","one"]
最简单的删除数组元素的方法就是设置length属性小于数组的最大索引
var arr=[1,2,3];
arr.length =2; // [1,2]
还可以使用delete运算符
var arr=[1,2,3];
delete arr[2];
2 in arr; //false,说明确实删除了
arr.length; //3 ,但是不会改变数组长度
用delete删除数组元素,就跟为其赋值为undefined是类似的,例如,都不会改变数组长度,都不会将元素从高索引处移下来填充已删除属性的空白,访问该位置都会返回undefined,唯一不同的是,删除元素,它是真的不存在了,他就是稀疏数组了
pop()方法,该方法删除数组的最后一个元素,并返回被移除的元素,原数组改变
var arr=[1,2,3];
arr.pop(); //3
arr; //[1,2]
和delete不同的是,pop()会改变数组的长度
shift()方法,该方法删除数组的第一个元素,并返回被移除的元素,原数组改变
var arr=[1,2,3];
arr.shift(); //1
arr; //[2,3]
和delete不同的是,shift()会改变数组的长度和后续元素的索引
有一个方法,集数组元素的添加、删除和替换功能于一身,并且更加灵活,不只是在开头和末尾
该方法用于在数组中插入,删除,替换元素,返回删除的元素组成的数组,原数组被改变
第一个参数,表示操作的起始位置
第二个参数,表示要删除的元素个数(0为不删除,省略则表示后续元素全部删除(有第三个参数的情况下,该参数不可省略))
第三~N个参数,表示要插入的项(没有,表示不插入)
插入操作(删除数为0)
var arr = [1,2,3,4]; // arr=[1,2,3,4]
b=arr.splice(1,0,5,"sss") // b=[] , arr= [1, 5, "sss",2,3, 4]
注: 同push()一样,splice()会插入数组本身,而不是数组的元素
删除操作
var arr = [1,2,3,4]; // arr=[1,2,3,4]
b=arr.splice(1,2) //b=[2,3] , arr=[1,4]
var arr = [1,2,3,4]; // arr=[1,2,3,4]
b=arr.splice(1) //b=[2,3,4] , arr=[1]
替换操作(删除的个数==插入元素的个数)
var arr = [1,2,3,4]; // arr=[1,2,3,4]
b=arr.splice(1,2,5,"sss") // b=[] , arr= [1, 5, "sss", 4]
不接收参数,用于反转数组项的顺序,返回反转后的数组,原数组改变
var arr = [1,2,3]; // arr=[1,2,3]
arr.reverse() ; // arr=[3,2,1];
该方法将数组中的元素排序,返回排序后的数组,原数组改变
当不带参数时,sort()是按照字符串的顺序进行升序排序(即使数组元素都是数字)
var arr=[1,2,3];
arr.sort(); //[1,2,3]
var arr=[2,1111,12];
arr.sort(); //[1111,12,2]
数组中包含undefined时,他们会被排在最后,而数组中包含null时,会按照首字母n排序,包含空字符串时,会排在最前面
[1,2,'o',null,'m','',undefined].sort();
//返回["", 1, 2, "m", null, "o", undefined]
通常,我们都不会希望按照字符串顺序排序,因此sort()方法可接受一个比较函数,比较函数有两个参数。分别代表每次排序比较时的两个数组项,如果第一个参数应该位于第二个之前则返回一个负数(即不改变位置),如果两个参数相等(顺序无关紧要)则返回0,如果第一个参数应该位于第二个之后则返回一个正数
var arr=[2,1111,12];
arr.sort(); //[1111,12,2]
arr.sort(function(x,y){
if(x<y){
return -1;
}
if(x>y){
return 1;
}
return 0;
}); // [2,12,1111] 此为升序排列,降序改变一下即可
简化写法
var arr=[2,1111,12];
arr.sort(function(x,y){
return x-y;
}); // [2,12,1111]此为升序排列,降序改变一下即可
乱序排列(双色球),让比较函数随机返回正数或者负数就可以了,而不需要根据两个参数的关系来返回(每两个数组项的比较,都会调用一次比较函数,每一次都是随机正负,并且排序的依据可以不由两个参数的关系决定,这是理解的重点啊)
arr.sort(function(){
return Math.random()>0.5 ? 1 : -1 ;
});
使用sort()方法还可以完成搜索操作,例如找出含有字母a的数组项(使用循环可能效率较低)
var arr = ["bc","abc","adf","erf","erde"];
arr.sort(function(x,y){
return x.indexOf("a")!=-1 ? -1 : 1 ;
});
//["abc", "adf", "erde", "erf", "bc"]
当告诉我们数组中有三个元素含有a字符的时候,我们就可以这样排序,然后前三项就是了
这个方法会先创建当前数组一个副本,原数组不变
在不传递参数的情况下,它只是复制当前数组并返回副本(浅度克隆)
如果传递给concat()方法的是一或多个数组,则该方法会将这些数组中的每一项都添加到结果数组中(注意,拼接的是数组的元素,而非数组本身,不同于push(如果数组的元素还是数组,那么,对不起,只好拼接数组了))
如果传递的值不是数组,这些值就会被简单地添加到结果数组中
var arr=[1,2,3];
arr.concat(4,5); //返回[1,2,3,4,5] ,arr仍是[1,2,3]
arr.concat([4],[5]); //返回[1,2,3,4,5] ,arr仍是[1,2,3]
arr.concat([[4]]); //返回[1,2,3,[4]] ,arr仍是[1,2,3]
接收一个或者两个数字参数,用于截取子数组,返回截取出的子数组,原数组不变
在只有一个参数的情况下,slice()方法返回从该参数指定位置开始到当前数组末尾的所有项。在拥有两个参数的情况下,slice()方法返回起始和结束位置(但不包括结束位置)之间的项
var arr = [1,2,3,4,5];
arr.slice(0,3); //[1,2,3]
arr.slice(0,-2); //[1,2,3]
arr.slice(2,2); //[]
需要注意的几点:
到目前为止:浅度克隆数组的三个方法:for循环,concat(),slice()
遍历数组,最常用的方法就是使用for循环
for(var i=0,len=arr.length;i<len;i++){
//数组的长度只查询一次,性能优化
//使用arr[i]
}
如果想要排除null、undefined和不存在的元素,则应该
for(var i=0,len=arr.length;i<len;i++){
if(!arr[i]){ //不准确,将会排除0,""等转换为false的值
continue;
}
//循环体
}
如果只想排除undefined和不存在的元素,则应该
for(var i=0,len=arr.length;i<len;i++){
if(arr[i]===undefined){
continue;
}
//循环体
}
如果想处理存在的undefined元素,但是跳过不存在的元素,则应该
for(var i=0,len=arr.length;i<len;i++){
if(!(i in arr)){
continue;
}
//循环体
}
还可以使用for/in循环来遍历数组,尤其是对于稀疏数组,不存在的索引将不会被遍历到
for(var index in arr){
//arr[index]
}
但是for/in循环能够枚举继承的可枚举属性,如添加到Array.prototype的属性,因此在数组上不应该使用for/in循环,除非使用额外的代码过滤不想要的属性,例如
for(var index in arr){
if(!arr.hasOwnProperty(index)){
continue;
}
//循环体
}
或者
for(var index in arr){
//跳过不是非负整数的索引(赋值给index的是字符串形式)
if(String(Math.floor(Math.abs(Number(index))))!==index){
continue;
}
}
并且for/in循环实现的遍历不一定是按照数组索引顺序的(特别的,如果数组同时拥有对象属性和数组元素),因此,如果程序依赖于遍历的顺序的话,就不要使用for/in循环了
首先,大部分ECMAScript5的数组方法的第一个参数接受一个函数A,并且对数组的每一个元素(或某些元素)调用一次该函数,该函数一般接收三个参数,分别表示数组元素、数组索引、数组本身(都可省略,但是顺序是一定的)
其次,大部分ECMAScript5的数组方法的第二个参数接受一个可选的对象,表示调用上面这个函数A的对象(也就是这个函数中this的指代)
该方法从头到尾遍历数组,为每个元素调用指定的函数,该方法没有返回值
而传递的函数接收三个参数:数组元素、数组索引、数组本身
var arr=[1,2,3,4,5];
var sum=0;
arr.forEach(function(value , index , arr){
sum+=value;
}) //sum为15
arr.forEach(function(value , index , arr){
arr[index]=value+1;
}) //arr为[2,3,4,5,6]
forEach()无法在所有元素都传递给调用的函数之前终止遍历,也就是,没有像for循环中的break语句,除非把forEach()方法放在一个try块中,并能抛出异常
该方法从头到尾遍历数组,为每个元素调用指定的函数,并返回一个数组,该数组由函数的返回值组成
var arr=[1,2,3,4,5];
b=arr.map(function(value , index , arr){
return value*value;
}) //b为[1,4,9,16,25]
传递给map()的函数必须要有返回值,否则返回的新数组就全是包含undefined了
map()返回的是新数组,不改变原有数组,原有数组是稀疏数组,返回的也是相同形式的稀疏数组(相同的长度,相同的确实元素)
该方法从头到尾遍历数组,为每个元素调用指定的函数,并返回一个数组,该数组是原数组的一个子集,函数返回true时,元素包含在子集中,反之,不包含
var arr=[1,2,3,4,5];
b=arr.filter(function(value , index , arr){
return value<4;
}) b为[1,2,3]
filter()方法会跳过稀疏数组中缺少的元素,它的返回数组总是稠密的,因此可以用来压缩稀疏数组
b=arr.map(function(value , index , arr){
return true;
})
还可以去除值为null和undefined的元素
b=arr.map(function(value , index , arr){
return x!=undefined && x!=null;
})
这两个方法遍历数组,为元素(不一定为每个元素)调用指定的函数,并返回true或者false
对于every(),当且仅当数组中所有元素在调用函数时都返回true,该方法才返回true
对于some(),至少有一个元素在调用函数时都返回true,该方法就返回true
var arr=[1,2,3,4,5];
arr.every(function(value,index,arr){
return value<10;
}); //返回true
arr.some(function(value,index,arr){
return value<2;
}); //也返回true
这两个方法都不一定从头到尾遍历数组,因为有经典的“短路问题”,对于every()来说,只要有一个元素时返回false,他就停止遍历,并返回false,对于some()来说,只要有一个元素时返回true,他就停止遍历,并返回true
根据数学惯例,在空数组上调用时,every()始终返回true,some()始终返回false
这两个方法从头到尾遍历数组,为每个元素调用指定的函数,并返回单个值(也被称为“注入”或者“折叠”)
var arr=[1,2,3];
var sum = arr.reduce(function(x,y){
return x+y;
},0); //数组求和
reduce()需要两个参数,第一个是执行简化操作的函数,第二个是一个传递给函数的初始值
函数的参数与前面的略有不同,可接受四个参数,分别是简化操作积累的结果、数组元素、数组索引、数组本身
第一次调用结果时,第一个参数就是初始值,第二次时,第一个参数就是上一次函数的返回值.........(如果省略了reduce()的第二个参数(初始值),那么将使用数组的第一个元素作为初始值,这意味着第一次简化操作使用了第一个元素和第二个元素)
reduceRight()和reduce()一样,只不过是从后向前处理数组
虽然看起来很眼熟,但是IE8及其以下浏览器是不支持的(字符串的indexOf()方法是都支持的)
这两个方法会查找指定元素的索引(前者从头查找,后者从末尾查找),返回该元素的索引或者没有找到的情况下返回-1
第一个参数表示要查找的元素(在比较第一个参数与数组中的每一项时,会使用全等操作符)
第二个参数,可选,表示查找的起始位置(默认从头或者从尾开始,可以是负数,现对于末尾的偏移量,或者说加上数组长度转换为整数)
var arr=[1,2,3,1,5];
arr.indexOf(1); //0
arr.indexOf(1,2); //3
arr.lastIndexOf(1); //3
arr.lastIndexOf(1,2); //0
灵活使用第二个参数,可以逐步不重复的去检索数组元素
数组是对象,但是数组有着很多常规对象没有的特性
但这不是定义数组的本质特性,因为可以把拥有一个数值length属性和对应非负整数属性的对象看成一种类型的数组——也就是类数组对象
类数组对象不能直接调用数组方法或者期望length属性有什么特殊的行为,但是仍然可以像数组一样遍历它们
//自己创建类数组对象
var obj = {};
var i;
for(i= 0;i<10;i++ ){
obj[i] = i*i;
}
obj.length = i;
//现在就可以当数组遍历了
for(var j= 0,len=obj.length;j<len;j++ ){
//
}
后续会碰到很多类数组对象,例如函数内部属性arguments,某些DOM方法返回的对象,jquery操作返回的对象....
检测类数组对象
function isArrayLike(o) {
if (o &&
Object.prototype.toString.call(o).slice(8,-1)!=="Array"&&
typeof o === "object" &&
isFinite(o.length) &&
o.length >= 0 &&
o.length===Math.floor(o.length) &&
o.length < 4294967296&& //2^32
o.nodeType!=3){ //排除文本节点
return true;
}else{
return false;
}
}
类数组对象没有继承自Array.prototype,所以不能直接调用数组方法,但是可以使用call方法调用
var obj ={"0":"a","1":"b","2":"c","length":3,} //这就是类数组对象
Array.prototype.join.call(obj,"+"); //返回 "a+b+c"
将类数组对象转换为真正的数组(当然也可以手动枚举,而且在IE8下面只能手动枚举(因为COM对象))
Array.prototype.slice.call(obj,0); //返回 ["a","b","c"] 返回真正数组的副本
在ECMAScript5中(其实从IE8开始就实现了),字符串的行为类似于只读数组
因为可以使用方括号([])语法代替字符串的charAt()方法,这样就会更加简洁,可读性高
var str="strong";
str.charAt(2); //返回"r"
str[2]; //返回"r"
除此之外,更通用的数组方法也可以用在字符串上
var str="strong";
Array.prototype.join.call(str,","); //返回 "s,t,r,o,n,g"
否则就只得这样实现了
var str="strong";
var arr = str.split('')
arr.join(); // "s,t,r,o,n,g"
字符串是不可变的,即使当成数组看待,它们也是只读的,而push()、sort()、reverse()、splice()等数组方法会修改数组,因此它们在字符串上是无效的