JavaScript权威指南--数组

知识要点

 

CXJ学习数组

 

数组是值的有序结合。每个值叫做一个元素,而每个元素在数组中都有一个位置,用数字表示,称为索引。

javascript数组是无类型的:数组的元素可以是任意类型,同个数组的元素类型不同也可以,还可以是对象或者嵌套其他数组。

javascript数组的索引是基于零的32位数值:第一个元素的索引为0,最大的索引为4 294 967 294(2的32次方-2),数组最大能容纳4 294 967 295个元素。

javascript数组是动态的:根据需要他们会增长或缩减,无需动态的去分配空间。

javascript数组可能是稀疏的:数组元素的索引不一定要连续的,针对稀疏数组,length比所有元素的索引要大。

1.创建数组

使用直接量创建数组是最简单方法,在方括号中将数组元素用逗号隔开即可。数组直接量中的值不一定是要常量,它们可以是任意的表达式,它可以包含对象直接量或其他数组直接量,如果省略数组直接量中的某个值,省略的值的元素将被赋予undefined。

调用构造函数Array()是创建数组的另一种方法。用三种方法调用构造函数

  • 调用时没有参数,创建一个空数组[];
  • 调用时有一个值是参数,它指定长度;
  • 显式指定两个或多个数组元素或者数组的一个非数组元素。

2.数组的读与写

使用[]操作符来访问数组中的一个元素。数组的引用位于方括号的左边。方括号中是一个返回非负整数值的任意表达式。使用该表达式可以读也可以写数组中的一个元素。

请记住:数组是对象的特殊形式。使用方括号访问数组元素就像使用方括号访问对象的属性一样。

数组的特别之处在于,当使用小于2的32次方的非负数整数作为属性名时,数组会自动维护其length属性值。

清晰地区分数组的索引和数组的属性名是非常有用的。所有的索引都是其属性名,但只有0到(2的32次方-2)的整数属性名才是索引。所有的数组都是对象,可以为其创建任意名字的属性。但如果使用的属性是数组的索引,数组的特殊行为就是将根据需要更新它们的length属性值,如果不是数组的索引则不会更新length。

注意,可以使用负数或非整数来索引数组。这种情况下,数值转换为字符串。字符串作为属性名来用。既然名字不是非负整数,就只能当做常规的对象属性,而非数组的所有。同样,如果凑巧使用了是非负整数的字符串,它就当做数组索引,而非对象的属性。当使用一个浮点数和一个整数相等时情况也是一样的:

a[-1.23] = true; //这将创建一个名为"-1.23"的属性,不会更新length
a["1000"] = 0; //这是数组的第1001个元素
a[1.000] //和a[1]相等

事实上数组索引仅仅是对象属性名的一种特殊类型,这意味着javascript数组没有“越界”错误的概念。当试图查询对象中不存在的属性时,不会报错。只会得到undefined值。类似于对象,对于对象同样存在这种情况。

既然数组是对象 ,那么它们可以从原型中继承元素。在ECMAScript5中,数组可以定义元素的getter和setter方法。如果一个数组确实继承了元素或使用了元素的getter和setter方法,你应该期望它使用非优化的代码路径:访问这种数组的元素的时间会与常规对象属性的查找时间相近。

3.稀疏数组

稀疏数组就是包含从0开始的不连续索引的数组。通常,数组的length属性值代表了数组中元素的个数。如果数组是稀疏的,那么length的值远大于元素的个数。可以使用Array构造函数或简单地指定数组的索引值大于当前数组长度来创建稀疏数组。

a = new Array(5);//数组中没有元素,但a.length值是5
a = []; //创建一个空数组,length的值为0
a[1000] = 0;//赋值添加一个元素,但设置的length值为1001
5 in a     //false
1000 in a   //true

足够稀疏的数组通常在实现上比稠密是数组更慢、内存利用率更高,在这样的数组中查找元素的时间与常规对象属性的查找时间一样长。

勘误:注意,当数组直接量中省略值时不会创建稀疏数组。省略的元素在数组中是存在的,其值为undefined。这和数组元素根本不在是有一些微妙的区别的。可以用in操作符检测两者的区别。这句有错

当使用for/in循环时,a1和a2的区别也很明显。

需要注意的是,当省略数组直接量中的值时(使用连续的逗号,比如[1,,,,3]),这时得到的也是稀疏数组,省略掉的值是不存在的:

var a1 = [, ]; //此时数组没有元素,长度为1
var a2 = [undefined]; //此时数组包含一个值为undefined的元素
0 in a1; //=>false: a1在索引0处没有元素
0 in a2; //=>true: a2在0处有一个值为undefined的元素

在一些旧版的实现中,如(firefox3),在存在连续逗号的情况下,插入undefined值的操作与此不同,在这些实现中,[1,,3]和[1,undefined,3]是一模一样的。

4.数组的长度

每个数组都有一个length属性。就是这个属性使其区别于常规的javascript。稠密数组和稀疏数组的比较。

在数组中,肯定找不到一个元素的索引值大于它的长度。为了维持此规则不变,数组有两个特殊行为。第一个如同上面的描述:如果为一个数组元素赋值,它的索引i大于或等于现有的数组的长度时,length的值将设置为i+1。第二个特殊行为就是设置length属性为一个小于当前长度的非负整数n时,当前数组中的那些索引值大于或等于n的元素将从中删除。

还可以将数组的length属性值设置为大于当前的长度。实际上不会向数组中添加新的元素,它只是在的尾部创建一个空的区域。

在ECMAScript5中,可以使用Object.defineProperty()将数组的属性变成只读的:

var a = [1, 2, 3];
Object.defineProperty(a, "length", {writable: false});     //让length属性只读
a.length = 0;   //a不会改变

类似的,如果让一个数组元素不能配置,就不能删除它。如果不能删除它,length的属性不能设置小于不可配置元素的索引值(见前文的Object.seal()和Object.freeze()方法)。

5.数组元素的添加和删除

添加数组元素最简单的方法:为新索引赋值;可以使用push()方法在数组的末尾增加一个或多个元素,在数组的末尾压入一个元素与给数组a[a.length]赋值是一样的;可以使用unshift()方法给首部插入一个元素,并且将其他元素移动到更高的索引处。

可以像删除对象属性一样使用delete运算符来删除数组元素:

a = [1, 2, 3];
delete a[1];
1 in a; //=>false:数组索引1并未在数组中定义
a.length; //=>3: delete操作并不影响数组的长度

删除数组元素与为其赋值undefined值是类似的(但有一些微妙的区别)。对一个数组元素使用delete不会修改数组的length属性;也不会将元素从高索引处移下来填充已经删除的元素空白。如果从一个数组中删除一个元素,它就变成稀疏数组。

数组由pop()方法(它和push一起使用),后者使减少长度1并返回删除元素的值。还有一个shift()方法(它和unshift()一起使用),从数组头部删除一个元素。和delete不同的是shift()方法将所有的元素下移到比当前元素索引低1的地方。

6.数组遍历

使用for循环是遍历数组的常用方法:

var keys = Object.keys(o); //获得o对象属性名组成的数组
var values = []; //在树组中存储匹配属性的值
for (var i = 0; i < keys.length; i++) { //对于数组中的每个索引
    var key = keys[i]; //获得索引处的键值
    values[i] = o[key]; //在values数组中保存属性值
}

在嵌套循环或其他它性能非常重要的上下文中,可以看到这种基本的数组遍历需要优化,数组的长度应该只是查询一次而非每次循环要查询:len = keys.length。

假如这些数组是稠密的,并且所有的元素都是合法数据。否则,使用数组元素之间都应该检测他们,如果想要排除null、undefined和不存在的元素

for (var i = 0; i < keys.length; i++) {
    if (!keys[i]) continue;//循环体
}

如果只想跳过undefined和不存在的元素:

for (var i = 0; i < keys.length; i++) {
     if (!keys[i] === undefined) continue; //跳过undefined和不存在的元素
    //循环体    
}

最后,如果只想跳过不存在的元素而仍然要处理存在的undefined的元素:

for (var i = 0; i < keys.length; i++) {
    if (!(i in keys)) continue; //跳过不存在的元素
}

还可以使用for/in循环,处理稀疏数组。循环每次将一个可枚举的属性名(包括数组索引)赋值给循环变量,不存在的索引将不会遍历到:

for (var index in sparseArray) {
    var value = sparseArray[index];
    //此处可使用索引和值做一些事情
}

在前面章节已经注意到for/in循环能够枚举继承的属性名,如添加到Array.prototype的方法。由于这个原因,在数组上不应该使用for/in循环,除非使用额外的检测方法来过滤掉不想要的属性。如下代码:取一即可:

for (var i in a) {
    if (!a.hasOwnProperty(i)) continue; //跳过继承的属性
    //循环体
}

for (var i in a) {
    //跳过不是非负整数的i
    if (String(Math.floor(Math.abs(Number(i)))) !== i) continue;
}

ECMAScript规范允许for/in循环以不同的顺序遍历对象的属性。通常数组元素的遍历实现是升序的,但不能保证一定是这样的。特别地,如果数组同时拥有对象属性和数组元素,返回的属性名很可能是按照创建的顺序而非数组的大小顺序。如何处理这个问题的实现各不相同,如果 算法依赖于遍历的顺序,那么最好不要使用for/in循环而使用常规的for循环。

ECMAScript5定义了一些遍历数组元素的新方法,按照索引的顺序按个传递给定义的一个函数。这些方法中最常用的就是forEach()方法。

var data = [1, 2, 3, 4, 5];
var sumOfSquares = 0; //得到数据的平方和
data.forEach(function(x) { //把每个元素传递给此函数
    sumOfSquares += x * x; //平方相加
});
sumOfSquares; //=>55: 1+4+9+16+25

forEach()和相关的遍历方法使得数组拥有拥有简单而强大的函数式编程风格。

7.多维数组

javascript不支持真正的多维数组,但可以用数组的数组来近似。访问数组的数组中的元素,只要简单地使用两次[]操作符即可。

 //创建一个多维数组
var table = new Array(10); //表格10行
for (var i = 0; i < table.length; i++)
    table[i] = new Array(10); //每行10列
 //初始化数组
for (var row = 0; row < table.length; row++) {
    for (col = 0; col < table[row].length; col++) {
        table[row][col] = row * col;
    }
}
 //使用多维数组来计算(查询)8*9
table[8][9]; //=>72

8.数组方法

ECMAScript3在Array.prototype中定义了一些很有用的操作数组的函数,这意味着这些函数作为人和数组的方法都是可用的。

8.1.join()

Array.join()方法将数组中所有元素都转化为字符串并连接在一起,返回最后生成的字符串。可以指定一个可选的字符串在生成的字符串中来分隔数组的各个元素。如过不指定分隔符,则使用默认的逗号。

var a = [2131, 551, 235, "hello", 123]
a.join(); //=>2131,551,235,hello,123
a.join(" "); //=> 2131 551 235 hello 123
a.join(""); //=>2131551235hello123
a.join("-"); //=> 2131-551-235-hello-123
var b = new Array(10);
b.join('-'); //=>---------9个连字号组成的字符串

Array.join()方法是String.split()方法的逆向操作,后者是将字符串分隔开来创建一个数组。

8.2.reverse()

Array.reverse()方法将数组的元素颠倒顺序,返回逆序数组。它采取了替换;换句话说,它不通过重新排列的元素创建新的数组,而是在原先的数组中重新排列它们。

8.3.sort()

Array.sort()方法将数组中的元素排序后并返回排序后的数组。当不带参数调用sort()时,数组元素以字母表顺序排序(如有必要将临时转化为字符串进行比较):

var a = new Array("banana", "cheery", "apple");
a.sort(); //=> ["apple", "banana", "cheery"]
var s = a.join(","); //=>"apple,banana,cheery"

如果数组包含undefined元素,它们会被排除到数组的尾部。

为了按照其它方式而非字母顺序表属性进行数组排序,必须给sort()方法传递一个比较函数。该函数决定了它的两个参数在排好序的数组中的先后顺序。假设第一参数应该在前,比较函数应该返回一个小于0的数值。并且,假设两个值相等(也就是说他们的属性无关紧要),函数应该返回0。因此,例如,用数值大小而非字母表顺序进行数组排序。代码如下:

var a = [33, 4, 1111, 222, 45555];
a.sort(); //=>[1111, 222, 33, 4, 45555] :字母表顺序
a.sort(function(a, b) { //=>[4, 33, 222, 1111, 45555] 数值顺序
return a - b; //根据数据,返回负数,0,正数
});
a.sort(function(a, b) {return b - a}); //=> [45555, 1111, 222, 33, 4] 数值大小相反的顺序

注意:这里使用匿名函数表达式非常方便。既然比较函数只使用了一次,就没必要给它们命名了。

另一个数组元素排序的例子 ,也许需要对一个字符串数组执行不区分大小写的字母表排序,比较函数首先将参数都转换为小写字符串(toLowerCase()方法),再开始比较:

a = ['ant', 'Bug', 'cat', 'Dog']
a.sort(); //=> ["Bug", "Dog", "ant", "cat"]:区分大小写的排序
a.sort(function(s, t) { //不区分大小写排序
var a = s.toLowerCase();
var b = t.toLowerCase();
if (a < b) return -1;
if (a > b) return 1;
return 0;
}); //=> ["ant", "Bug", "cat", "Dog"]

8.4.concat()

Array.concat()方法创建并返回一个新数组,它的元素包含调用concat()原始数组的元素和concat()的每个参数。如果这些参数中任何一个自身是数组,则连接的是数组的元素,而非数组本身。但要注意:concat()不会递归扁平化数组的数组。concat()也不会修改调用的属性。

var a = [1, 2, 3];
a.concat(4, 5); //=> [1, 2, 3, 4, 5]
a.concat([4, 5]); //=>[1, 2, 3, 4, 5]
a.concat([4, 5], [6, 7]); //=>[1, 2, 3, 4, 5, 6, 7]
a.concat(4, [5, [6, 7]]); //=>[1, 2, 3, 4, 5, [6,7]]

8.5.slice()

Array.slice()方法返回指定数组的一个片段或子数组。它的两个参数分别指定了片段的开始和结束的位置。返回的数组包含第一个参数和所有到但不含第二个参数定制的位置之间的所有数组元素。如果只指定一个参数,则返回数组将从开始的位置到数组结尾的所有元素。如果参数中出现了负数,它表示相对于数组中最后一个元素的位置。例如参数“-1”指定了最后一个元素,而-3指定了倒数第三个元素。注意slice()不会修改调用的数组。

var a = [1, 2, 3, 4, 5];
a.slice(0, 3); //=> [1, 2, 3]
a.slice(3); //=> [4, 5]
a.slice(1, -1); //=> [2, 3, 4]
a.slice(-3, -2); //=> [3]

8.6.splice()

Array.splice()方法是在数组中插入或删除元素的通用方法。不同于slice()和concat(),splice()会修改调用的数组。

splice()能够从数组中删除元素,插入元素到数组中或者同时完成这两种操作。在插入或删除点之后的数组元素会根据需要增加或减少它们的索引值,因此数组的其它部分仍然保持连续。

splice()第一个参数指定了插入(或)删除的起始位置。第二个参数指定了应该从数组中删除的元素的个数。如果省略第二个参数,从起点开始到数组结尾的所有元素都将被删除。splice()返回一个由删除元素组成的数组,或者没有删除就返回一个空数组。

var a = [1, 2, 3, 4, 5, 6, 7, 8];
a.splice(4); //=> [5, 6, 7, 8] ,a是 [1, 2, 3, 4]
a.splice(1,2) //=> [2, 3] ,a是 [1, 4]
a.splice(1,1) //=>[4],a是[1]

splice()的前两个参数指定了要删除的数组的元素。紧随其后的任意个参数指定了需要插入数组中的元素,从第一个参数指定的位置开始插入。例如

var a = [1, 2, 3, 4, 5];
a.splice(2,0,'a','b')//=>返回[],a的值是[1, 2, "a", "b", 3, 4, 5]
a.splice(2,2,[1,2],3)//=>返回 ["a", "b"],a的值是[1,2,[1,2],3,3,4,5]

注意区别于concat(),splice()会插入数组本身而非数组的元素。

8.7.push()和pop()

push()和pop()方法允许将数组当做栈来使用。push()方法在数组的尾部添加一个或多个元素,并返回新的数组长度。pop()方法相反:它删除数组的最后一个元素,减小数组长度并返回它删除的值。注意:两个方法都修改并替换原始数组而非生成一个修改版的新数组。

var stack = []; //stack:[]
stack.push(1, 2); //stack:[1,2] 返回2
stack.pop(); //stack:[1] //返回2
stack.push(3); //stack:[1,3] //返回2
stack.pop(); //stack:[1] //返回3
stack.push([4, 5]); //stack:[1,[4,5]] //返回2
stack.pop() //stack:[1] //返回[4,5]
stack.pop() //stcck:[] //返回1

8.8.unshift()和shift()

unshfit()在数组的头部添加一个或多个元素,并将已存在的元素移动到更高的索引的位置来获得足够的空间,最后返回数组的新长度。shift()删除数组的第一个元素并将其返回。然后并把所有的元素下移一个位置来填补数组头部的空缺。

var a = []; //a:[]
a.unshift(1); //a:[1] 返回1
a.unshift(22); //a:[22,1] 返回2
a.shift(); //a:[1] 返回22
a.unshift(3, [4, 5]); //a:[3,[4,5],1] 返回3
a.shift(); //a:[[4,5],1] 返回3
a.shift(); //a:[1] 返回[4,5]
a.shift(); //a:[] 返回1

注意:当使用多个参数调用unshift()时它的行为令人惊讶。参数是一次性插入(就像splice()方法),而非一次一个。这意味着最终的数组中插入的元素的顺序和他们在参数列表中的顺序一致。而加入元素一个一个插入,它们的顺序应该是反过来的。

8.9.toString()和toLocaleString()

该方法将其每个元素转化为字符串(如有必要调用元素的toString() 方法),并且输出逗号分隔的字符串列表。注意,输出不包括方括号或其它任何形式的包裹数值的分隔符。例如:

[1, 2, 3, "hello"].toString(); //=>'1,2,3,hello'
["a", "b", "c", "d"].toString(); //=>'a,b,c,d'
[1, 2, [4, 5, 6], 7].toString(); //=>'1,2,4,5,6,7'

注意,这里与不使用任何参数的join()方法返回的字符串是一样的。

toLocaleString()是toString()方法的本地化版本。它调用元素的toLocaleString()方法将每个数组元素转化为字符串,并使用本地化(和自定义实现的)分隔符将这些字符串连接起来生成最终的字符串。

9.ECMAScript5中的数组方法

ECMAScript5定义了9个新的方法来遍历、映射、过滤、检测、简化和搜索数组。

首先,大多数方法的第一个参数接收一个函数,并且对数组的每个元素(或几个元素)调用一次该函数。如果是稀疏数组,对不存在的元素不调用传递的函数。 在大多数情况下,调用提供的函数提供三个参数:数组元素、元素的索引和数组本身。 通常,只需要第一个参数值,第二个参数是可选的。如果有两个参数,则调用的函数被看做是第二个参数的方法。也就是说:在调用函数时候传递进去的第二个参数作为它的this关键字值来使用。被调用函数的返回值非常重要,但是不同的方法处理返回值的方式也不一样。ECMAScript5中的数组方法都不会修改他们调用的原始数组。当然传递这些方法的函数是可以修改这些数组的。

9.1.forEach()方法

forEach()方法从头到尾遍历数组,为每个元素调用指定的函数。如上所述:传递的函数作为forEach()的第一个参数。然后forEach()使用3个个参数调用该函数:数组元素、元素的所有和数组本身。如果只关心数组元素的值,可以编写只有一个参数的函数——额外的参数将忽略。

var data = [1, 2, 3, 4, 5]; //要 求和的数组
 //计算数组的和
var sum = 0; //初始值0
data.forEach(function(value) {
    sum += value; //将每个值累加到sum上
});
sum; //=>15
 //给每个数组元素自加1
data.forEach(function(v, i, a) {
    a[i] = v + 1;
});
data; //=> [2, 3, 4, 5, 6]

注意的是:forEach()无法在所有元素都传递给调用的函数之前终止遍历。也就是说,没有像for循环中使用的相应的break语句。如果要提前终止。必须把forEach()方法放在一个try块中,并能抛出一个异常。如果forEach()调用的函数能抛出foreach.break异常。循环会提前终止:

function foreach(a, f, t) {
    try {
        a.forEach(f, t);
    } catch (e) {
        if (e === foreach.break) return;
        else throw e;
    }
}
foreach.break = new Error("StopIteration");

9.2.map()方法

map()方法调用的数组的每个元素传递给指定的函数,并返回一个数组,它包含该函数的返回值。例如:

a = [1, 2, 3]
b = a.map(function(x) {
    return x * x;
});
b; //=> [1, 4, 9]

传递给map()的函数调用方式和传递给forEach的调用方式一样。但传递给map()的函数应该有返回值。注意:map()返回是的新数组,它不修改调用的数组。如果是稀疏数组,返回的也是相同方式的稀疏数组:它具有相同的长度,相同的缺失元素。

9.3.filter()

filter()方法返回的数组元素是调用的数组的一个子集。传递的函数是用来逻辑判定的:该函数返回true或false。调用判定函数就像调用forEach()和map()一样。如果返回值为true或能转化为true的值,那么传递给判定函数的元素就是这个子集的成员,它将被添加到一个作为返回值的数组中。例如:

a = [5, 4, 3, 2, 1];
smallvalues = a.filter(function(x) {
    return x < 3
}); //=> [2, 1]
everyother = a.filter(function(x, i) {
    return i % 2 == 0
});//=> [5, 3, 1]

注意:filter()跳过稀疏数组中缺少的元素,它返回的数组总是稠密的。为了压缩稀疏数组的空缺。代码如下:

var dense = sparse.filter(function() {
    return true;
});

甚至压缩空缺并删除undefined和null元素,可以这样使用filter():

a = a.filter(function(x) {
    return x !== undefined && x != null;
});

9.4.every()和some()

every()和some()方法是数组的逻辑判定:他们对数组元素应用指定的函数进行判定,返回true或false。

every()函数就像数学中的“征对所有”的量词∀ :当且仅当征对数组中的所有元素调用判定函数都返回true,它才返回true:

var a = [1, 2, 3, 4, 5];
a.every(function(x) {return x < 10;}); //=>true:所有的值都小于10
a.every(function(x) {return x % 2 === 0;}); //=>fasle :不是所有的值都是偶数

some()方法就像数学中的"存在"量词彐:当数组中只要有一个元素调用判定函数返回true,它就返回true;并且当且仅当数值中所有元素判定调用函数都返回false,它才返回false.

var a = [1, 2, 3, 4, 5];
a.some(function(x) {return x % 2 === 0;}); //true :a含有偶数值
a.some(isNaN) //=>false a不包含非数值元素

注意,一旦every()和some()确认该返回什么值什么值它们就会停止遍历数组元素。some()在判定函数第一次返回true后就返回true,但如果判定函数一直返回false,它将遍历整个数组。every()恰好相反:它在 判定函数第一次返回false后就返回false,但如果判定函数一直返回true,它将遍历整个数组。注意:根据数学上的惯例,在空数组时,every()返回true,some()返回false。

9.5.reduce()和reduceRight()

reduce()和reduceRight()方法使用指定的函数将数组元素进行组合,生成单个值。这在函数编程中是常见的操作。也可以称为“注入”和“折叠”,例举说明它是如何工作的。

var a = [1, 2, 3, 4, 5];
var sum = a.reduce(function(x, y) {return x + y}, 0); //数组求和
var produce = a.reduce(function(x, y) {return x * y}, 1); //数组求积
var max = a.reduce(function(x,y){return (x>y)?x:y;}); //求最大值

reduce()需要两个参数,第一个是执行化简操作的函数。化简函数的任务就是用某种方法把两个值组合成化简一个值,并返回化简后的值。在上述例子中,函数通过加法,乘法或最大值法合成两个值。第二个(可选)的参数是第一个传递给函数的初始值。

reduce()使用的函数与forEach()和map()使用的函数不同。比较熟悉的是,数组元素、元素的索引和数组本身作为第2-4个参数传递给函数。第一个参数是到目前为止的化简操作累计的结果。第一次操作函数时,第一个参数是一个初始值,它就是传递给reduce()的第二个参数。在接下来的调用中,这个值就是上一次化简函数的返回值。在上面的第一个例子中,第一次调用化简函数时的参数是0和1。这两者相加并返回1.在此调用的时的参数是1和2,它返回3.然后计算3+3=6,6+4=10,最后计算10+5=15.最后的值是15.reduce()返回这个值。

可能已经注意到了,上面三次调用reduce()时只有一个参数:没有指定初始值。当不指定初始值调用reduce()时,它将使用数组的第一个元素作为其初始值。这意味着第一次调用化简函数就使用了第一个和第二个数组元素作为其第一个和第二个参数。在上面求和和求积的例子中,可以省略初始值参数。

在空数组上,不带初始值参数调用reduce()导致类型错误异常。如果调用它的时候只有一个值--数组只有一个元素并且没有指定初始值,或者有一个空数组并且指定一个初始值--reduce()只是简单地返回那个值而不会调用化简函数。

reduceRight()工作原理和reduce()一样,不同的是它按照数组索引从高到低,(从右至左)处理数组,而不是从低到高。如果简化操作的优先顺序是从右到左,你可能想使用它,例如:

var a = [2, 3, 4];
 //计算2^(3^4)。乘方操作的优先顺序是从右到左
var big = a.reduceRight(function(accumlator, value) {
    return Math.pow(value, accumlator);
});

注意:reduce()和reduceRight()都能接受一个可选参数,它指定了化简函数调用的this关键字的值。可选的初始值参数仍然需要占一个位置。如果想要化简函数作为一个特殊对象的方法调用,请参看function.blind()方法。

值得注意的是,上面描述的every()和some()方法是一种类型的数组化操作。但是不同的是,他们会尽早终止遍历而不总是访问每一个数组元素。

为了简单起见,到目前位置所展示的例子都是数值的,但数学计算不是reduce()和reduceRight()的唯一意图。考虑下6.2中的union()函数,它计算两个对象的并集,并返回另一个新对象,所以它的工作原理和一个简化函数一样,并且可以使用reduce()来把它一般化,计算任意数目对象的“并集”。

var objects =[{x:1},{y:2},{z:3}];
var merged = objects.reduce(union); //=>{x:1,y:2,z:3}

回想一下,当两个对象拥有同名的属性时,union()函数使用第一个参数的属性值,这样,reduce()和reduceRighet()在师院union()时给出了不同的结果:

var objects =[{x:1,a:1},{y:2,a:2},{z:3,a:3}]; 
var leftunion = objects.reduce(union); //=>{x: 1, a: 3, y: 2, z: 3}
var rightunion = objects.reduceRight(union); //=> {z: 3, a: 1, y: 2, x: 1}

9.6.indexOf()和lastIndexOf()

indexOf()和lastIndexOf()搜着这个数组时具有给定值的元素,返回找到的第一个元素的所有或者没有找到就返回-1.indexOf()从头至尾搜索,而lastIndexOf()则反方向搜索。

a = [0, 1, 2, 1, 0];
a.indexOf(1); //=>1:a[1]是1
a.lastIndexOf(1); //=>3:a[3]是1
a.indexOf(3) //=>-1:没有找到值为3的元素

对于本节描述的其它方法,indexOf()和lastIndexOf()方法不接受一个函数作为其参数,第一个参数需要搜索的值,第二个参数是可选的:它指定数组中的一个索引,从哪里开始搜索。如果省略该参数,indexOf()从头开始搜索,而lastIndexOf()从末尾开始搜索。第二个参数也可以是负数,它表示相对数组末尾的偏移量。

如下函数在一个数组中搜索指定的值并返回包含所有匹配的数组索引的一个数组,它展示了如何运用 indexOf的第二个参数来查找除了第一个以外匹配的值。

//在数组中查找所有出现的x,并返回一个包含匹配索引的数组
function findall(a, x) {
    var results = [], //将会返回的数组
        len = a.length, //待搜索数组的长度
        pos = 0; //开始搜索的位置
    while (pos < len) { //循环搜素多个元素...
        pos = a.indexOf(x, pos); //搜素
        if (pos === -1) break; //未找到,就完成搜素
        results.push(pos); //否则,在数组中存储索引
        pos = pos + 1; //并从下一个位置开始搜索
    }
    return results; //返回包含索引的数组
};

console.log(findall([1,2,5,3,4,5,6,7,5], 5))   //[2,5,8]

注意,字符串也有indexOf()和lastIndexOf()方法,它们和数组方法功能类似。

10.数组类型

数组是具有特殊行为的对象。给定一个未知的对象,判定它是否为数组通常非常有用,在ECMAScript5中,可以使用Array.isArray()函数来做这件事情

Array.isArray([]); //true
Array.isArray({}); //false

但是在ECMAScript5以前,要区分数组和非数组对象却令人惊讶的困难。typeof()操作符在这里帮不上忙:对数组它只返回:object(并且对除了函数以外的对象都是如此)。instanceof操作符只能用于简单情形:

[] instanceof Array; //=>true
({}) instanceof Array; //=>false

使用instanceof的问题是在web浏览器中有可能有多个窗口或窗体(frame)存在。每个窗口都有自己的JavaScript环境,有自己的全局对象。并且,每个全局对象有自己的一组构造函数。因此一个窗体中的对象将不可能是另外窗体中的构造函数的实例。窗体之间的混淆不常发生,但这个问题足以证明instanceof操作符不能视为一个可靠的数组检测方法。

解决方案是检查对象的类属性,对数组而言改属性的值总是“Array”,因此在ECMAScript3中isArray()函数代码可以这样写

var isArray = Function.isArray || function(o) {
    return typeof o === "object" && Object.prototype.toString.call(o) === "[object Array]";
};

11.类数组对象

我们已经看到,javascript数组的有一些特性是其它对象所没有的:

  • 当有新的元素添加到列表时,自动更新length属性
  • 设置length为一个较小的值将截断数组
  • 从Array.prototype中继承一些有用的方法。
  • 其类属性为"Array"

这些特性让javascript数组和常规的对象有明显的区别,但是他们不是定义数组的本质特性。一种常常完全合理的看法把拥有一个数值length属性和对于非负整数属性的对象看做一种类型的数组。

实践中这些“类数组”对象实际上偶尔出现,虽然不能再他们上直接调用数组的方法或者期望length属性有什么特殊行为,但是仍然可以用针对真正数组遍历的代码来实现遍历它们,结论就是很多数组算法征对类数组对象工作得很好,就像征对真正的数组一样。如果算法把数组看成只读的或者如果它们至少保持数组长度不变,也尤其是这种情况。以下代码为一个常规对象增加了一些属性使其变成类数组对象,然后遍历生成伪数组“元素”:

var a = {}; //从一个常规空对象开始
 //添加一组属性,称为“类数组”
var i = 0;
while (i < 10) {
    a[i] = i * i;
    i++;
}
a.length = i;
console.log(a) //现在当真正的数组遍历它
var total = 0;
for (var j = 0; j < a.length; j++)
    total += a[j];
console.log(total)

Arguments对象就是一个类数组的对象。在客户端javascript中,一些DOM方法(如document.getElementsByTagName())也返回类数组对象。下面有一个函数 可以用来检测类数组对象

 //判定o是否是一个类数组对象
 //字符串和函数都length属性,但是他们可以有typeOf检测将其排除
 //在客户端javascript中,DOM文本节点也有length属性,需要用额外的o.nodetype != 3将其排除
function isArrayLike(o) {
    if (o && //o非null、undefined等
        typeof o === "object" && //o是对象
        isFinite(o.length) && //o.length是有限数
        o.length >= o && //o.length是非负数
        o.length === Math.floor(o.length) && //o.length是整数
        o.length < 4294967296) //o.length < 2^32
        return true;
    else
        return fasle; //否则它不是
}

javascript数组方法是特意定义为通用的,因此他们不仅应用在正真的数组,而且在类数组对象上都能正常工作。在ECMAScript5中,所有数组方法都是通用的。在ECMAScript3中,除了toString()和toLocaleString以为所有的方法也是通用的(concat()方法是一个特例:虽然可以用在类似的数组对象上,但它没有将那个对象扩充进返回的数组中)。既然类数组没有继承Array.prototype,那就不能再它上面直接调用数组方法。尽管如此,可以间接的使用Function.call方法调用:

var a ={"0":"a","1":"b","2":"c",length:3}
Array.prototype.join.call(a,"+") //=>"a+b+c"
Array.prototype.slice.call(a,0) //=>["a","b","c"]:真正的数组副本
Array.prototype.map.call(a.Function(x){
  return x.toUpperCase();
}) //=>["A","B","C"] 

ECMAScript1.5数组方法是在Firefox 1.5中引入的。由于它们写法的一般性,Firefox还将这些方法的版本在Array构造函数上直接定义为函数,使用这些方法定义的版本,上个例子可以这么写:

var a = {"0": "a","1": "b","2": "c",length: 3};
Array.join(a, "+");
Array.slice(a, 0);
Array.map(a, function(x) {
    return x.toUpperCase();
})

用在类数组对象上时,数组方法的静态函数版本方法非常有用。但既然它们不是标准的,不能期望它们在所有浏览器都有定义。可以这样书写代码保证它们之前是存在的

Array.join = Array.join || function(a, serp) {
    return Array.prototype.join.call(a, serp);
};
Array.slice = Array.slice || function(a, form, to) {
    return Array.prototype.slice.call(a, form, to);
};
Array.map = Array.map || function(a, f, thisArg) {
    return Array.prototype.map.call(a, f, thisArg);
};

12.作为数组的字符串

在ECMAScript5(在众多最新浏览器已经实现--包括ie8,遭遇ECMAScript5)中,字符串的行为类似于只读数组。除了用charAt()方法来访问单个字符以外,还可以使用方括号

var s = "test";
s.charAt(0); //=> "t"
s[0] ;  // =>"t"

当然征对字符串的typeOf操作符仍然返回"String",但是如果给Array.isArray()传递字符串,它将返回false.

可索引字符串的最大好处就是简单,用方括号代替了chartAt()调用,这样更加简洁、可读性更高效。不仅如此,字符串的行为类似于数字的事实是的通用数组方法可以应用到字符串上。例如:

var s = "javascript"
Array.prototype.join.call(s, " "); //=>' j a v a s c r i p t'
Array.prototype.filter.call(s, function(x) { //过滤字符串中的字符
        return x.match(/[^aeiou]/); //匹配非元音字符
    }).join("") //=>jvscrpt

请记住,字符串是不可变值,故当他们作为数组看待时,他们是只读的。如push()、sort()、reverse()、splice()、等数组方法会修改数组,它们在字符串上是无效的。不仅如此,使用数组方法修改字符串会导致错误,出错的时候没有提示。

 

posted @ 2017-03-15 23:12  chenxj  阅读(276)  评论(0编辑  收藏  举报