1. JavaScript中的数组
在C++、Java中,数组是一种高效的数据结构,随机访问性能特别好,但是局限性也特别明显,就是数组中存放的数据必须是同一类型的,而在JavaScript中,数组中的每一项数据可以是任何类型,也就是说数组中可以同时存放Number类型,Object类型和其他类型,这一点类似于PHP的关联数组。本质上来说JavaScript中的数组是对象,在访问数组的数据时其实是在访问它的属性,这一点可以用以下的代码证明:
var colors=["red","blue","yellow"]; console.log(typeof colors); //object console.log(typeof Array); //function console.log(colors instanceof Array); //true console.log(Array.isArray(colors)); //true //console.log(colors.0); //语法错误 //console.log(colors.1); //语法错误 console.log(colors[0]); //red console.log(colors["0"]); //red colors["my"]="purple"; colors.sayColors=function(){ console.log(this.toString()); }; console.log(colors.my); //purple console.log(colors["my"]); //purple colors.sayColors(); //red,blue,yellow console.log(colors.length); //3 console.log(colors.sayColors); //function ()... for(key in colors){ console.log(key +": "+colors[key]); } /* 0: red 1: blue 2: yellow my: purple sayColors: function (){ console.log(this.toString()); } */
以上代码中,首先创建了一个名为colors的数组,然后使用typeof运算符检查colors的类型,得到的结果是object,所以JavaScript中的数组是对象。之后的代码中还检查了Array的类型,结果是function,在JavaScript中创建数组有两种方式,第一种方式就是上述代码中的字面量方式,另一种方式是 new Array()的方式,这种方式是不是很眼熟啊?对,这就是创建对象的一种方式,这样就明白了,Array是数组的构造函数,更证明了数组是对象,而数组与生俱来的方法pop(),push(),join()等等都是来自于Array.prototype。更详细的介绍请看我另外一篇博客《JavaScript高级特性-对象、原型、函数之间的关系》。因为JavaScript中的数组是对象,所以它才可以存放任何数据。之后的代码中还给colors添加了属性my和方法sayColors(),而且也都调用成功了。所有的事情有利就有弊,JavaScript中数组能混合存放数据,得益于它是对象,因此它的随机访问性能并没有C++、Java中的数组好。以C++举例,有如下代码:
#include#include using namespace std; int main(){ int a[10]; for(int i=0;i<10;i++){ a[i]=rand(); } cout<2]; return 0; }
当程序运行到标注的代码时,首先在内存中开辟一块连续的空间,由于int类型在C++中占用4个字节,声明的数组容量是10,所以这块空间的大小是4*10B,然后将这块空间的起始地址赋值给变量a。
当访问数组中的变量时,首先在内存中拿出a存储的值,也就是数组连续空间的起始地址,然后再根据下标计算要访问的数据的内存地址,计算规则是:数组起始地址+4*数组下标。
然后再根据地址访问数据。也就是说访问数组中的数据时,在不考虑cache的情况下,需要访问两次内存,一次是取出a的值,另一次才是取出数据(在程序编译之后,变量名就变成了内存中的地址,只不过是相对地址,在实际运行时,操作系统会加上一定的偏移量,详细介绍请查阅编译原理和操作系统内存管理部分的资料)。
以上述代码举例,当我访问数组中下标为2的数据时,首先到内存中找到数组a的值,然后计算出a[2]的地址是:a的值+4*2,然后访问这个地址就得到a[2]的值,这个过程中访问了两次内存。
2. 创建数组的方式
创建数组的两种基本方式为:一是使用Array构造函数,二是使用数组字面量表示法。使用Array构造函数创建数组和使用构造函数创建对象一样,格式如下:
var colors=new Array();
其中构造函数Array可以不传入参数,也可以传入参数。当不传参数时,创建的数组长度为0;
var colors=new Array(); console.log(colors.length); //0 for(var i=0;i){ console.log(colors[i]); }
传入参数时,情况就比较复杂,当参数是一个整数时,就创建长度为这个整数的数组,数组中所有项的值为undefined:
var colors=new Array(4); console.log(colors.length); //4 for(var i=0;i){ console.log(colors[i]); //undefined undefined ... }
如果参数是一个非数值时(参数为浮点数时报错RangeError),就创建一个包含该参数的项数为1的数组:
var colors=Array("color"); console.log(colors.length); //1 for(var i=0;i){ console.log(colors[i]); //color
参数也可以是数组中应该保存的项,项之间用逗号分隔:
var colors=new Array("red","blue","yellow"); console.log(colors.length); //3 for(var i=0;i){ console.log(colors[i]); //red blue yellow }
使用Array构造函数的时候也可以省略new操作符:
var colors=Array(2,34,5); console.log(colors.length); //3 for(var i=0;i){ console.log(colors[i]); //2 34 5 }
另一种创建数组的方式就是类似对象的字面量方式,叫做数组的字面量表示法:
var colors=["red","blue","yellow"];
将所有的数据项放在方括号内,项与项之间用逗号隔开,最后一项不用逗号,因为在最后一项之后添加逗号可能造成在不同的浏览器处理的结果不一样,例如:
var friends=["yangyule","vile",]; var number=[ , , ,];
第一行代码中在IE8及之前的版本中,数组friends会包含三项,分别为“yangyule”,“vile”,undefined,而在其它浏览器中friends包含两项,分别是“yangyule”和“vile”。第二行代码中,在IE8及之前的版本中会创建一个包含四个值为undefined的数组,而在其它浏览器中会创建包含三个值为undefined的数组。所以在使用数组字面量创建数组的时候最后一项最好不要添加逗号。
3. 数组属性
数组的方法有一大堆,而天生的属性只有一个,就是length,表示数组的长度,这个属性很有特点,因为它不是只读的,通过它可以给数组设置长度:
var nums=[1,2,3]; console.log(nums.length); //3 nums.length=10; for(var i=0;i<10;i++){ console.log(nums[i]); //1 2 3 undefined ... } nums.length=2; for(var i=0;i<3;i++){ console.log(nums[i]); //1 2 undefined }
如果将数组的length值赋值为小于数组当前长度的值,则将会删除数组末尾的项;如果赋值为大于数组当前长度的值,则新增的每一项都会被赋值为undefined
4. 检查数组
这个问题本来应该是不存在的,检查数组很简单,因为数组是对象,是Array的实例,所以直接用 array_name instanceof Array就可以的,但是上述的情况仅仅适用于一个网页,或者说是一个全局执行环境,在网页中如果嵌有
javascript30.html
DOCTYPE html>
<html>
<head>
<title>javascripttitle>
head>
<body>
<script type="text/javascript">
var colors=["red","blue","yellow"];
console.log(colors instanceof Array); //true
script>
<iframe name="child" src="childFream.html">iframe>
body>
child_frame.html
DOCTYPE html>
<html>
<head>
<title>childtitle>
head>
<body>
<script type="text/javascript">
console.log(parent.colors instanceof Array); //false
script>
body>
html>
上述代码注释给出了输出结果,当父页面(javascript30.html)在自己的全局环境中执行instanceof时,输出结果是true,当在子页面(child_frame.html)中调用instanceof判断父页面中的colors时,输出结果是false,这个原因要怪instanceof操作符,因为使用instanceof运算符的时候,它的操作过程就是沿着要判断的对象的原型对象一直向上查找,如果最后对象的原型对象和instanceof的右值的原型是同一个对象,也就是说地址是一样的,都指向同一个对象,那么结果就为true,否则为false。(由instanceof操作符原理引出的经典问题,使用instanceof判断函数是否是Object类型的实例,返回结果是true,更多介绍请看我另一篇博客《JavaScript高级特性-对象、原型、函数之间的关系》)由于上述的原因,ECMAScript5新增了Array.isArray()方法,专门用来判定数组。
5. 数组方法
数组的默认方法可谓是所有内置对象中最多的了,有一大堆,下面我们分门别类的介绍
5.1 转换方法
在JavaScript中所有对象都有toLocaleString(),toString(),valueOf()方法,这是因为Object的原型对象中存在这些方法,数组也是对象,所以数组也有这些方法,不同的是Array的原型对象重写了toLocaleString(),toString()方法,而valueOf()方法是继承Object而来的。
数组的toString()方法会返回由数组中的每一项值的字符串形式拼接而成的以逗号分隔的字符串,实际上创建这个字符串的时候是分别调用每一项的toString()方法。
数组的toLocaleString()方法常常返回与toString()方法相同的结果,也是以逗号分隔的每一项的字符形式组成的字符串,但是这两个方法唯一不同的地方是当数组调用toLocaleString()方法时,调用的是每一项的toLocaleString()方法。为了验证上述,做个小小的试验:
person1={ toString:function(){ return "调用了person1的toString()"; }, toLocaleString:function(){ return "调用了person1的toLocaleString()"; } }; person2={ toString:function(){ return "调用了person2的toString()"; }, toLocaleString:function(){ return "调用了person2的toLocaleString()"; } }; var person=[person1,person2]; console.log(person.toString()); //调用了person1的toString(),调用了person2的toString() console.log(person.toLocaleString()); //调用了person1的toLocaleString(),调用了person2的toLocaleString()
这样的结果就比较好接受了吧?一目了然
数组的valueOf()方法返回的结果还是数组,这个方法是从Object中继承而来的
数组的join()方法只接受一个参数,这个参数用于指定分隔函数返回数组的字符串的分隔符,也就是说,此函数返回数组的字符串形式,字符串中的分隔符用参数指定,在上面代码的末尾加上一句:
console.log(person.join("|")); //调用了person1的toString()|调用了person2的toString()
从结果也可以看出,其实在执行join()方法的时候也调用了数组每一项的toString()方法。
5.2 栈方法
JavaScript中的数组提供了两个方法可以让数组类似数据结构中的栈,这两个方法分别是push()和pop(),其中push类似于栈中的入栈,可以将任意多的元素放入数组的末尾,并返回当前数组的length;pop()类似于出栈,可以将数组的最后一项移除,并返回移除的哪一项。
var nums=[]; nums.push(21,23,4); nums.push(12); nums.push(2,34); for(var i=0;i){ console.log(nums[i]); //21 23 4 12 2 34 } console.log(nums.pop()); //34 for(var i=0;i ){ console.log(nums[i]); //21 23 4 12 2 } console.log(nums.pop()); //2 console.log(nums.pop()); //12 for(var i=0;i ){ console.log(nums[i]); //21 23 4 }
5.3 队列方法
JavaScript中数组的shift()和push()方法能使它模拟数据结构中的队列,shift()方法可以移除数组中第一项并返回该项,而push()方法则可以将数据添加到数组的末尾,所以一个先进先出的队列就形成了。
var nums=new Array(23,43,5,3,25,57,4); for(var i=0;i){ console.log(nums[i]); //23 43 5 3 25 57 4 } console.log(nums.shift()); //23 for(var i=0;i ){ console.log(nums[i]); //43 5 3 25 57 4 } nums.push(8,45); console.log(nums.shift()); //43 console.log(nums.shift()); //5 for(var i=0;i ){ console.log(nums[i]); //3 25 57 4 8 45 }
除了使用上述的两个函数实现队列外,还可以使用unshift()和pop()方法。unshift()方法和shift()方法作用相反,unshift()方法可以在数组的前端添加任意多的数据并返回修改后数组的长度,而pop()方法则可以将数组末尾项删除,所以可以用这两个方法来模拟一个反向的队列。
var nums=new Array(23,43,5,3,25,57,4); for(var i=0;i){ console.log(nums[i]); //23 43 5 3 25 57 4 } console.log(nums.pop()); //4 for(var i=0;i ){ console.log(nums[i]); //23 43 5 3 25 57 } nums.unshift(8,45); console.log(nums.pop()); //57 console.log(nums.pop()); //25 for(var i=0;i ){ console.log(nums[i]); //8 45 23 43 5 3 }
重排序方法
JavaScript的数组中有两个可以将数组进行重排序的方法:reverse()和sort()。reverse()方法是用来将数组的顺序逆置的,sort()方法可以将数组排序,默认是按升序排列
var nums=Array(1,2,3,4,5,6,7); console.log(nums.toString()); //1,2,3,4,5,6,7 nums.reverse(); console.log(nums.toString()); //7,6,5,4,3,2,1 nums.sort(); console.log(nums.toString()); //1,2,3,4,5,6,7
这样做看似没有什么问题,但是换做以下代码,问题就显露出来了:
var nums2=[1,5,10,11,15,20,24,30]; console.log(nums2.toString()); //1,5,10,11,15,20,24,30 nums2.sort(); console.log(nums2.toString()); //1,10,11,15,20,24,30,5
可以看出,原本排好序的数组经过调用sort()函数之后反而乱了,这是因为sort()函数没有参数传入的时候,会调用数组每一项的toString()方法,然后进行字符串的比较,所以得到如上的结果。如果想让sort()函数按照意愿排序,就需要将一个比较函数传给sort(),这个比较函数必须有两个参数,如果第一参数必须在第二个参数之前就返回一个负数,如果两个参数相等,返回0,如果第一个参数应该位于第二个参数之后就返回一个正数
var nums2=[1,5,10,11,15,20,24,30]; console.log(nums2.toString()); //1,5,10,11,15,20,24,30 nums2.sort(function(value1,value2){ return value1-value2; }); console.log(nums2.toString()); //1,5,10,11,15,20,24,30
5.5 操作方法
数组中的操作方法concat()可以基于现在的数组创建一个新的数组,在concat()方法没有参数的时候,仅仅执行的是创建当前数组的一个副本并返回;如果传给concat()方法数组或者是值,这些值和数组中的项会依次添加到新创建的数组中的末尾,然后返回。concat()方法并不会修改原数组。
var colors=["red"]; console.log(colors.toString()); //red console.log(colors.concat().toString()); //red console.log(colors.toString()); //red console.log(colors.concat(["blue","yellow"],"black").toString()); //red,blue,yellow,black console.log(colors.toString()); //red
数组的slice()方法可以有选择的将原数组中的项复制到新创建的数组中,在slice()方法没有参数的时候,仅仅是返回当前数组的拷贝数组;当slice()函数有一个参数的时候,表示要复制的数组项开始位置,一直从此位置复制到数组末尾;当slice()函数有两个参数的时候,表示要复制数组项的起始位置和结束位置。slice()函数中的参数是从0开始的。slice()函数并不会影响原始数组。
var colors=["red","blue","yellow","black"]; console.log(colors.toString()); //red,blue,yellow,black console.log(colors.slice().toString()); //red,blue,yellow,black console.log(colors.toString()); //red,blue,yellow,black console.log(colors.slice(2).toString()); //yellow,black console.log(colors.toString()); //red,blue,yellow,black console.log(colors.slice(1,2).toString()); //blue console.log(colors.toString()); //red,blue,yellow,black
数组还有一个splice()方法来操作数组,这个方法操作数组中的项,可以删除项,插入项,替换项。其实这个方法就是把删除数组中的项和把数据插入数组封装在了一起。当使用splice()函数删除数组中的某些项时,需要提供两个参数,分别是开始删除项的位置和删除的项数;当使用splice()函数插入项的时候,需要提供三种类型的参数,分别是插入的起始位置,0(要删除的项),和插入的项,从第三个参数开始都表示要插入数组中值;当使用splice()替换数组中的项的时候也需要提供三种类型的参数,第一便是起始位置,第二是删除的项数,之后所有的项表示要插入数组的值。splice()函数始终会返回一个数组,这个数组中包含被删除的项,如果没有删除的项,则返回空数组。
var colors=["red","blue","yellow","black","green","purple","orange","white"]; //删除数组中的项 console.log(colors.toString()); //red,blue,yellow,black,green,purple,orange,white console.log(colors.splice(1,4).toString()); //blue,yellow,black,green console.log(colors.toString()); //red,purple,orange,white //把数据插入数组 console.log(colors.splice(1,0,"blue").toString()); //返回空数组,输出空白 console.log(colors.toString()); //red,blue,purple,orange,white console.log(colors.splice(2,0,["yellow","black"]).toString()); //返回空数组,输出空白 console.log(colors.toString()); //red,blue,yellow,black,purple,orange,white //替换数组中的项 console.log(colors.splice(1,3,"black").toString()); //blue,yellow,black,purple console.log(colors.toString()); //red,black,orange,white
上述代码中,用concat()函数分别实现了删除数组,插入数组和替换数组。
5.6 位置方法
ECMAScript5为数组新增了两个位置方法,分别是indexOf()和lastIndexOf(),这两个方法都是根据参数查找设定值在数组中的位置,不同的是indexOf()从前往后查,而lastIndexOf()是从后往前查,但都是返回第一个匹配元素的位置,换句话说就是返回该值在数组中的索引位置,这两个方法都接受一个必须参数和一个可选参数,第一个参数是要查找的值,是必需的,第二个参数是开始查找的索引位置,这个参数是可选的,没有此参数的时候均是由第一个开始往后逐个查找,直到找到匹配项,如果找完数组也没有找到,则返回-1。
var colors=["red","blue","yellow","black","green","purple","yellow","orange","white"]; console.log(colors.indexOf("yellow")); //2 console.log(colors.indexOf("yellow",2)); //2 console.log(colors.indexOf("yellow",3)); //6 console.log(colors.lastIndexOf("yellow")); //6
5.7 迭代方法
ECMAScript5为数组定义了五个迭代方法,每个方法接受两个参数,一个必需参数和一个可选参数,必须参数也就是第一个参数必须是一个函数,而且这个函数接收三个参数,分别代表数组项的值,和这个项在数组中的位置还有数组本身;而可选的参数是运行参数函数的作用域对象。这五个方法分别是:
every():对数组中的每一项运行给定的函数,如果该函数对于数组的每一项都返回true,则返回true。
filter():对数组中的每一项运行给定的函数,返回给定函数返回true的项组成的数组。
forEach():对数组中的每一项运行给定的函数,此方法没有返回值。
map():对数组中的每一项运行给定的函数,返回每次调用函数返回结果的数组。
some():对数组中的每一项运行给定的函数,如果该函数对任一项返回true,则返回true。
这些方法其实见名知意,其中的every()和some()就像是逻辑运算符与和或,every()用于判断数组中所有的项是否都满足一个条件,所有的项都满足就返回true,而some()则是用于查找数组中是否有满足条件的项,有就返回true。filter()就更明显了,就是一个过滤器,返回满足过滤条件的那些数组项。forEach()就是用来输出数组的数据用的,没啥特别的。map()很明显就是映射,使用场景就是将数组中所有的项进行一次处理,将所有的处理结果返回。
var nums=[1,2,4,5,6,7,8]; //判断数组中的数据是否全部大于0 console.log(nums.every(function(item,index,array){ if(item>0) return true; })); //判断数组中是否有大于8的项 console.log(nums.some(function(item,index,array){ if(item>=8) return true; })); //遍历数组 nums.forEach(function(item,index,array){ console.log(item); }); //过滤小于5的项 console.log(nums.filter(function(item,index,array){ if(item>=5) return true; }).toString()); //将数组中所有的项加3再除以2 console.log(nums.map(function(item,index,array){ return (item+3)/2; }).toString());
5.8 归并方法
ECMAScript5为数组新增了两个归并方法reduce()和reduceRight(),这两个函数都会遍历数组,处理数组中的数据项,然后返回一个最终的值。这两个方法都接收两个参数,第一个参数是在每一项上调用的函数,第二个是最为归并的基础值(此参数可选),作为参数的函数接收四个参数:前一个值,当前值,当前项的索引和数组对象,这个函数的返回值会作为下次调用此函数的第一个参数,也就是作为前一个值。这两个函数唯一不同的就是:reduce()函数从数组第一项开始遍历数组,而reduceRight()从数组最后一项遍历。
var nums=[1,334,5,6,6,8,7678,34,32]; console.log(nums.reduce(function(){ //8104 return arguments[0]+arguments[1]; }));
上面的代码中,作为参数的函数并没有形式参数,但依然可以正常调用,这和JavaScript中函数特性有关。在JavaScript中,只要你愿意,所有的函数都可以不声明参数,而通过内部的arguments对象使用传来的实参,也就是说,JavaScript其实并不关注你的形式参数,在调用函数的时候,即使函数没有形式参数也可以向其传入实参,这时引用实参就可以通过函数的内部属性arguments来实现,详细介绍请看我的另外一篇博客《JavaScript高级特性-函数》。