张啸


世界上最快乐的事,莫过于为理想而奋斗。


JS(3) 数组

数组是值的有序集合,每个值叫做一个元素,而每个元素在数组中有一个位置,以数字表示,称为索引。JavaScript数组的索引是基于零的32位数,第一个元素的索引为0,最大可能的索引为4 294 967 2942e32-2),数组最大能容纳4 294 967 295个元素。

JavaScript数组可能是稀疏的:数组元素的索引不一定要连续的,它们之间可以有空缺。每个JavaScript数组都有一个length属性。针对非稀疏数组,该属性就是数组元素的个数。针对稀疏数组,length比所有元素的索引要大。

JavaScript数组是JavaScript对象的特殊形式,数组索引实际上和碰巧是整数的属性名差不多。我们将在本文的其他地方更多地讨论特殊化的数组。通常,数组的实现是经过优化的,用数字索引来访问数组元素一般来说比访问常规的对象属性要快很多。

一、创建数组

使用数组直接量是创建数组最简单的方法,在方括号中将数组元素用逗号隔开即可

1
2
3
var empty = [];
var primes = [2, 3, 4, 5, 11];
var misc = [1.1, true, 'a', ];

数组直接量中的值不一定要是常量,它们可以是任意的表达式

1
2
3
4
5
6
7
var base = 1024;
var table = [table, base+1, base+2, base+3];

var b = [[1, {x:1, y:2}], [2, {x:3, y:4}]];

var count = [1, ,3]; // 数组有3个元素,中间那个元素值为undefined
var undefs = [,,]; // 数组有2个元素,都是undefined

数组直接量的语法允许有可选的结尾的逗号,故[,,]只有两个元素而非三个。

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

  • 调用时没有参数

    该方法创建一个没有任何元素的空数组,等同于数组直接量[]

    1
    var a = new Array()
  • 调用时有一个数值参数,它指定长度

    该方法创建指定长度的数组。当预先知道所需元素个数时,这种形式的Array构造函数可以用来预分配一个数组空间。注意,数组中没有存储值,甚至数组的索引属性还未定义

    1
    var a = new Array(10)
  • 显示指定两个或多个数组元素或者数组的一个非数值元素

    以这种形式,构造函数的参数将会称为新数组的元素

    1
    var a = new Array(5, 4, 3, 2, 1, 'testing, testing');

二、数组元素的读和写

使用[]操作符来访问数组中的一个元素。数组的引用位于方括号的左边。方括号是一个返回非负整数值的任意表达式。使用该语法既可以读又可以写数组的一个元素。因此,下方代码都是合法的

1
2
3
4
5
6
7
var a = ['world'];
var value = a[0];
a[1] = 3.14;
i = 2;
a[i] = 3;
a[i+1] = 'hello';
a[a[i]] = a[0];

请记住,数组是对象的特殊形式

使用方括号访问数组元素就像用方括号访问对象的属性一样。JavaScript将指定的数字索引值转换成字符串——索引值1变成“1”——然后将其作为属性名来使用。关于索引值从数字转换为字符串没什么特别之处:对常规对象也可以这么做

1
2
var o = {};
o[1] = 'one';

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

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

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

1
2
3
4
5
6
// 这将创建一个名为“-1.23”的属性
a[-1.23] = true;
// 这是数组的第1001个元素
a['1000'] = 0;
// 和a[1]相等
a[1.000];

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

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


三、稀疏数组

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

1
2
3
a = new Array(5);       // 数组没有元素,但是a.length=5
a = []; // 创建一个空数组,length=0
a[1000] = 0; // 赋值添加一个元素,但是length=1001

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

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

1
2
3
4
var a1 = [,,,];
var a2 = new Array(3);
0 in a1; // false a1在索引0处有一个元素
0 in a2; // false a2在索引0处没有元素

四、数组长度

每个数组都有一个length属性,就是这个属性使其区别于常规的JavaScript对象。针对稠密数组(非稀疏数组),length属性值代表数组中元素的个数。其值比数组中最大的索引大1

1
2
[].length;                  // 0 数组没有元素
['a', 'b', 'c'].length // 3 最大索引值为2,length=3

在数组中肯定找不到一个元素的索引值大于或等于它的长度,为了维持此规则不变化,数组有两个特殊的行为

  • 如果为一个数组元素赋值,它的索引i大于或等于现有数组的长度是,length属性的值将设置为i+1

  • 设置length属性为一个小于当前长度的非负整数n时,当前数组中那些索引值大于或等于n的元素将从中删除

1
2
3
4
a = [1, 2, 3, 4, 5];
a.length = 3; // a = [1, 2, 3]
a.length = 0; // a = []
a.length = 5; // 长度为5,但是没有元素,就像new Array(5)

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

在ES5中,可用用Object.defineProperty()让数组的length属性变成只读的。

1
2
3
4
5
6
a = [1,2,3];
Object.defineProperty(a, 'length', {
writable: false
});

a.length = 0; // a不会改变

类似的,如果让一个数组元素不能配置,就不能删除它。如果不能删除它,length属性就不能设置为小于不可配置元素的索引值。


五、数组元素的添加和删除

  • push()

    我们可以使用push()方法在数组末尾增加一个或多个元素

    1
    2
    3
    a = [];
    a.push('zero'); // a = ['zero']
    a.push('one', 'two'); // a = ['zero', 'one', 'two']

    在数组尾部压入一个元素与给数组a[a.length]赋值是一样的。

  • unshift()

    可以使用unshift()方法在数组的首部插入一个元素,并且将其他元素依次移到更高的索引处。

  • delete

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

    删除数组元素与为其赋值undefined值是类似的(但有一些微妙的区别)。

    注意,对一个数组元素使用delete不会修改数组的length属性,也不会将元素从高索引处移下来填充已删除属性留下的空白。如果从数组中删除一个元素,它就变成稀疏数组。

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

    可以使用pop()方法,使长度减少1并返回被删除元素的值

  • shift()

    可以使用shift()方法,从数组头部删除一个元素,和delete不同的是,shift()方法将所有元素下移到比当前索引低1的地方

  • splice()

    可以使用splice()方法来插入、删除或替换数组元素,它会根据需要修改length属性并移动元素到更高或更低的索引处


六、数组遍历

使用for循环是遍历数组元素最常见的方法

1
2
3
4
5
6
var keys = Object.keys(o);
var values = [];
for (var i= 0; i < keys.length; i++) {
var key = keys[i];
values[i] = o[key];
}

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

1
2
3
for (var i = 0, len = keys.length; i < len; i++) {
// 循环体不变
}

这些例子假设数组是稠密的,并且所有的元素都是合法数组。否则,使用数组元素之前应该先检测它们。如果想要排除nullundefined和不存在的元素

1
2
3
4
for (var i = 0; i < a.length; i++) {
if (!a[i]) continue;
// 循环体
}
1
2
3
4
5
for (var i = 0; i < a.length; i++) {
// 如果只想跳过undefined和不存在的元素
if (a[i] === undefined) continue;
// 循环体
}
1
2
3
4
5
for (var i = 0; i < a.length; i++) {
// 如果只想跳过不存在的元素,仍然要处理存在的undefined元素
if (!(i in a)) continue;
// 循环体
}

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

1
2
3
4
for (var index in sparseArray) {
var value = sparseArray[index];
// 循环体
}

但是由于for/in循环能够枚举继承的属性名,如添加到Array.prototype中的方法,在数组上不应该使用for/in循环,除非使用额外的检测方法来过滤不想要的属性

1
2
3
4
5
6
7
8
9
10
11
for (var i in a) {
// 跳过继承的属性
if (!a.hasOwnProperty(i)) continue;
// 循环体
}

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

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

ES5定义了一些遍历数组元素的新方法,按照索引的顺序按个传递给定义的一个函数

1
2
3
4
5
6
var data = [1, 2, 3, 4, 5];
var sumOfSquares = 0;
data.forEach(function(x) {
sumOfSquares += x * x;
})
sumOfSquares;

七、多维数组

JavaScript不支持真正的多维数组,但可以用数组的数组来近似。访问数组的数组中的元素,只要简单地使用两次[]操作符即可。例如,假设变量matrix是一个数组的数组,它的基本元素是数值,那么matrix[x]的每个元素是包含一个数值数组,访问数组中特定数值的代码为matrix[x][y]

1
2
3
4
5
6
7
8
9
10
11
12
var table = new Array(10)
for (var i = 0; i < table.length; i++) {
table[i] = new Array(10)
}

for (var row = 0; row < table.length; row++) {
for (col = 0; col < table[row].length; col++) {
table[row][col] = row * col;
}
}

var product = table[5][7]; // => 35

八、数组方法

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

  • join()

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

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

    1
    2
    3
    4
    5
    6
    var a = [1,2,3];
    a.join(); // => '1,2,3'
    a.join(' '); // => '1 2 3'
    a.join(''); // => '123'
    var b = new Array(10);
    b.join('-'); // => '---------'
  • reverse()

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

    1
    2
    3
    var a = [1, 2, 3];
    var b = a.reverse(); // => [3, 2, 1]
    a == b; // => true
  • sort()

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

    1
    2
    3
    var a = new Array('banana', 'cherry', 'apple');
    a.sort();
    var s = a.join(','); // 'apple,banana,cherry'

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    var a = [33, 4, 1111, 222];
    a.sort(); // [1111, 222, 33, 4]
    a.sort(function(a, b) {
    return a - b;
    }); // [4, 33, 222, 1111]
    a.sort(function(a, b) {
    return b - a;
    }); // [1111, 222, 33, 4]

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

4. concat()

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

1
2
3
4
5
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]]

5. slice()

Array.slice()方法返回指定数组的一个片段或子数组。它的两个参数分别指定了片段的开始和结束位置。返回的数组包含第一个参数指定的位置和所有到但不含第二个参数指定的位置之间的所有数组元素。如果只指定一个参数,返回的数组将包含从开始位置到数组结尾的所有元素。如参数中出现负数,它表示相对于数组的最后一个元素的位置。

注意,slice()方法不会修改调用的数组

1
2
3
4
5
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]

6. splice()

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

splice()能够从数组中删除元素、插入元素到数组中或者同时完成这两种操作。在插入或删除点之后的数组元素会根据需要增加或减小它们的索引值,因此数组的其它部分仍然保持连续的。splice()第一个参数指定了插入和(或)删除的起始位置。第二个参数指定了应该从数组中删除的元素的个数。如果省略第二个参数,从起始点开始到数组结尾的所有元素都将被删除。splice()返回一个由删除元素组成的数组,或者如果没有删除元素就返回一个空数组

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

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

1
2
3
var a = [1,2,3,4,5];
a.splice(2, 0, 'a', 'b'); // return [] a = [1,2,'a','b',3,4,5]
a.splice(2, 2, [1, 2], 3); // return ['a', 'b'] a = [1,2,[1,2],3,3,4,5]

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

7. push()和pop()

push()和pop()方法允许将数组当作栈来使用。push()方法在数组的尾部添加一个或多个元素,并返回数组新的长度。pop()方法则相反,它删除数组的最后一个元素,减小数组长度并返回它删除的值。组合使用push()和pop()能够用JavaScript数组实现先进后出的栈

1
2
3
4
5
6
7
8
var stack = [];
stack.push(1, 2); // return 2 stack = [1,2]
stack.pop(); // return 2 stack = [1]
stack.push(3); // return 2 stack = [1,3]
stack.pop(); // return 3 stack = [1]
stack.push([4, 5]); // return 2 stack = [1,[4,5]]
stack.pop(); // return [4,5] stack = [1]
stack.pop(); // return 1 stack = []

8. unshift()和shift()

unshift()和shift()方法的行为类似于push()和pop(),不一样的是前者是在数组的头部而非尾部进行元素的插入和删除

1
2
3
4
5
6
7
8
var a = [];
a.unshift(1); // return 1 a = [1]
a.unshift(22); // return 2 a = [22,1]
a.shift(); // return 22 a = [1]
a.unshift(3, [4, 5]); // return 3 a = [3,[4,5],1]
a.shift(); // return 3 a = [[4,5],1]
a.shift(); // return [4,5] a = [1]
a.shift(); // return 1 a = []

注意,当使用多个参数调用unshift()时,参数是一次性插入的,而非一次一个地插入。这意味着最终的数组中插入的元素的顺序和它们在参数列表中的顺序一致。

9. toString()和toLocaleString()

数组和其他JavaScript对象一样拥有toString()方法。针对数组,该方法将其每个元素转化为字符串(如有必要将调用元素的toString()方法)并且输出用逗号分隔的字符串列表。注意,输出不包括方括号或其他任何形式的包裹数组值的分隔符

1
2
3
[1, 2, 3].toString();                     // '1,2,3'
['a', 'b', 'c'].toString(); // 'a,b,c'
[1, [2, 'c']].toString(); // '1,2,c'

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

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


九、ES5中的数组方法

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

ES5数组方法大多数的第一个参数接收一个函数,并且对数组的每个元素调用一次该函数。如果是稀疏数组,对不存在的元素不调用传递的函数。在大多数情况下,调用提供的函数使用三个参数:数组元素、元素的索引和数组本身。第二个参数是可选的,如果由第二个参数,则调用的函数被看作是第二个参数的方法,也就是说,在调用函数时传递进度的第二个参数作为它的this关键字的值来使用。

1. forEach()

forEach()方法从头至尾遍历数组,为每个元素调用指定的函数

1
2
3
4
5
6
7
8
9
10
var data = [1,2,3,4,5];
var sum = 0;
data.forEach(function(value) {
sum += value;
});
sum; // => 15
data.forEach(function(v, i, a) {
a[i] = v + 1;
});
data; // => [2,3,4,5,6]

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

1
2
3
4
5
6
7
8
9
10
11
12
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');

2. map()

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

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

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

3. filter()

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

1
2
3
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()会跳过稀疏数组中缺少的元素,它的返回数组总是稠密的。为了压缩稀疏数组的空缺,代码如下

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

甚至,压缩空缺并删除undefined和null元素

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

4. every()和some()

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

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

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

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

1
2
3
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

5. reduce()和reduceRight()

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

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

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

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

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

reduceRight()的工作原理和reduce()一样,不同的是它按照数组索引从高到低处理数组,而不是从低到高。

在对象章节中,我们定义了一个union()函数,它计算两个对象的“并集”,并返回另一个新对象,新对象具有二者的属性。该函数期待两个对象并返回另一个对象,所以它的工作原理和一个化简函数一样,并且可以使用reduce()来把它一般化,计算任意数目的对象的“并集”

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

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

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

6. indexOf()和lastIndexOf()

indexOf()和lastIndexOf()搜索整个数组中具有给定值的元素,返回找到的第一个或最后一个元素的索引,如果没有找到就返回-1。

1
2
3
4
var a = [0,1,2,1,0];
a.indexOf(1); // 1 a[1] == 1
a.lastIndexOf(1); // 3 a[3] == 1
a.indeOf(3); // -1

indexOf()和lastIndexOf()方法不接收函数作为其参数,第一个参数是需要搜索的值,第二个参数是可选的,它指定数组中的一个索引,从指定位置开始搜索。第二个参数也可以是负数,它代表数组末尾的偏移量。

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


十、数组类型

在ES5中,可以使用Array.isArray()函数来判断未知的对象是否为数组

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

ES5以前,要区分数组和非数组对象却令人惊讶地困难。typeof操作符对数组返回“对象”(并且对于除了函数以外的所有对象都是如此)。instanceof操作符只能用于简单的情形

1
2
[] instanceof Array;                        // true
({}) instanceof Array; // false

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

解决方案是检查对象的类属性,对数组而言该属性的值总是“Array”

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

十一、类数组对象

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

  • 当有新的元素添加到列表中时,自动更新length属性

  • 设置length为一个较小值将截断数组

  • 从Array.prototype中继承一些有用的方法

  • 其类属性为“Array”

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = {};
// 添加一些属性,称为“类数组”
var i = 0;
while( i < 10) {
a[i] = i * i;
i++;
}
a.length = i;

// 现在,当作真正的数组遍历它
var total = 0;
for (var j = 0; j < a.length; j++) {
total += a[j];
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
判定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 >= 0 && // o.length为非负值
o.length === Math.floor(o.length) && // o.length是整数
o.length < 4294967296>) // o.length < 2e32
return true;
else
return false;
}

后文中我们可以看到在ES5中字符串的行为与数组类似,然而,类似上述的类数组对象的检测方法针对字符串常常返回false——它们最好当作字符串处理,而非数组。

JavaScript数组方法是特意定义为通用的,因此它们不仅应用在真正的数组而且在类数组对象上都能正确工作。除了toString()和toLocaleString()以外的方法都是通用的。

concat()方法是一个特例,虽然可以用在类数组对象上,但它没有将那个对象扩充进返回的数组中。

既然类数组对象没有继承自Array.prototype,那就不能在它们上面直接调用数组方法。尽管如此,可以间接地使用Function.call方法调用。

1
2
3
4
5
6
var a = {'0': 'a', '1': 'b', '3': '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']

十二、作为数组的字符串

在ES5中,字符串的行为类似于只读的数组。除了用charAt()方法来访问单个的字符以外,还可以使用方括号

1
2
3
var s = 'test';
s.charAt(0); // 't'
s[1]; // 'e'

当然,针对字符串的typeof操作符仍然返回”string”,但是如果给Array.isArray()传递字符串,它将会返回false。

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

1
2
3
4
5
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()等数组方法会修改数组,它们在字符串上是无效的。不仅如此,使用数组方法来修改字符串会导致错误:出错的时候没有提示。


参考文献

  1. 《JavaScript权威指南(第6版)》