创建数组

创建数组的最简单方法是使用数组字面值:

1
2
3
var empty = []; // An array with no elements
var primes = [2, 3, 5, 7, 11]; // An array with 5 numeric elements
var misc = [1.1, true, "a",]; // 3 elements of various types + trailing comma

数组字面值中的元素可以是任意的表达式。如果数组中有省略的元素,其值为undefined

1
2
var count = [1,,3]; // An array with 3 elements, the middle one undefined.
var undefs = [,,]; // An array with 2 elements, both undefined.

另一种方式是使用Array构造函数:

1
2
3
var a = new Array(); // equivalent to []
var a = new Array(10); // length 10, but has no values
var a = new Array(5, 4, 3, 2, 1, "testing, testing"); // arguments become array's elements

读写数组元素

使用[]运算符访问数组的元素。所有索引都是属性名,但只有0到2^32-1之间的整数属性名才是索引。所有数组都是对象,你可以在数组上创建任何名称的属性。如果你使用的是索引,那么数组会自动更新length属性。

负数或浮点数可以用于数组索引。它们被转换为一个字符串,并被用作属性名,它被视为常规对象属性,而不是数组索引。

数组索引只是一种特殊类型的属性名,这意味着数组没有“越界”的概念:

1
2
3
a = [true, false]; // This array has elements at indexes 0 and 1
a[2] // => undefined. No element at this index.
a[-1] // => undefined. No property with this name.

稀疏数组

稀疏数组是没有连续索引的数组。稀疏数组的length属性的值大于元素的数量:

1
2
3
a = new Array(5); // No elements, but a.length is 5.
a = []; // Create an array with no elements and length = 0.
a[1000] = 0; // Assignment adds one element but sets length to 1001.

创建数组时忽略的元素和稀疏数组中不存在的元素是有区别的:

1
2
3
4
var a1 = [,,,]; // This array is [undefined, undefined, undefined]
var a2 = new Array(3); // This array has no values at all
0 in a1 // => true: a1 has an element with index 0
0 in a2 // => false: a2 has no element with index 0

数组长度

数组的length属性使得数组不同于普通的JavaScript对象。对于密集的数组, length属性指定数组中元素的数量。

1
2
[].length // => 0: the array has no elements
['a','b','c'].length // => 3: highest index is 2, length is 3

稀疏数组的length 属性大于元素的数量。也就是说,数组的索引永远小于数组length属性。为了维持这个不变量,数组有2个特殊行为:

  1. 如果给大于length的索引i的元素赋值,则length被设为i+1
  2. 如果将length属性设置为小于其当前值的非负整数n,那么任何索引大于或等于n的元素都被删除。

length属性设置为大于其当前的值,这样做实际上只是在数组末尾创建一个稀疏区域。在ES5中,可以使用Object.defineProperty将数组的length属性设置为只读:

1
2
3
a = [1,2,3]; // Start with a 3-element array.
Object.defineProperty(a, "length", {writable: false});  // Make the length property readonly.
a.length = 0; // a is unchanged.

类似地,如果使数组元素不可配置,则不能删除它,即不能将length属性设置为小于不可配置元素的索引。

添加和删除元素

最简单的添加元素方法是赋值给新索引:

1
2
3
a = [] // Start with an empty array.
a[0] = "zero"; // And add elements to it.
a[1] = "one";

还可以使用push方法向数组末尾添加一个或多个值:

1
2
3
a = []; // Start with an empty array
a.push("zero") // Add a value at the end. a = ["zero"]
a.push("one", "two") // Add two more values. a = ["zero", "one", "two"]

可以使用unshift在数组开头插入元素,将现有的元素移到更高的索引。

使用delete删除数组元素:

1
2
3
4
a = [1,2,3];
delete a[1]; // a now has no element at index 1
1 in a // => false: no array index 1 is defined
a.length // => 3: delete does not affect array length

delete数组元素不会改变length属性,也不会将索引较高的元素向下移动,成为稀疏数组。也通过将length属性设置为新的长度来删除数组末尾的元素。

pop方法将数组的length减1,并返回删除元素的值。

shift方法从数组的开头删除一个元素,并将所有的元素索引前移一个位置。

splice是用于插入、删除或替换数组元素的通用方法。它改变length属性,并根据需要将数组元素移到更高或更低的索引。

遍历数组

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

1
2
3
4
5
6
var keys = Object.keys(o); // Get an array of property names for object o
var values = [] // Store matching property values in this array
for(var i = 0; i < keys.length; i++) { // For each index in the array
    var key = keys[i]; // Get the key at that index
    values[i] = o[key]; // Store the value in the values array
}

for循环一个小小的优化,不用每次查询length

1
2
3
for(var i = 0, len = keys.length; i < len; i++) {
    // loop body remains the same
}

循环中检查数组元素的几种情况:

1
2
3
4
5
6
7
8
9
for(var i = 0; i < a.length; i++) {
    if (!a[i]) continue; // Skip null, undefined, and nonexistent elements
}
for(var i = 0; i < a.length; i++) {
    if (a[i] === undefined) continue; // Skip undefined + nonexistent elements
}
for(var i = 0; i < a.length; i++) {
    if (!(i in a)) continue ; // Skip nonexistent elements
}

for/in循环可以用于稀疏阵列。每次循环给循环变量分配一个可枚举的属性名(包括数组索引),不存在的索引将不会被遍历:

1
2
3
4
for(var index in sparseArray) {
    var value = sparseArray[index];
    // Now do something with index and value
}

for/in循环可以返回继承属性的名字,比如添加到Array.prototype的属性。因此在数组上使用for/in循环,应该过滤掉不需要的属性:

1
2
3
4
5
6
7
for(var i in a) {
    if (!a.hasOwnProperty(i)) continue; // Skip inherited properties
}
for(var i in a) {
    // Skip i if it is not a non-negative integer
    if (String(Math.floor(Math.abs(Number(i)))) !== i) continue;
}

ES5定义了许多用于迭代数组元素的新方法,forEach方法是最通用的一个:

1
2
3
4
5
6
var data = [1,2,3,4,5]; // This is the array we want to iterate
var sumOfSquares = 0; // We want to compute the sum of the squares of data
data.forEach(function(x) { // Pass each element of data to this function
    sumOfSquares += x*x; // add up the squares
});
sumOfSquares // =>55 : 1+4+9+16+25

多维数组

JavaScript不支持真正的多维数组,但可以用数组的数组模拟它们:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Create a multidimensional array
var table = new Array(10); // 10 rows of the table
for(var i = 0; i < table.length; i++)
    table[i] = new Array(10); // Each row has 10 columns
// Initialize the array
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

join方法将数组元素转换为字符串,并连接起来,返回字符串。可以指定一个可选的字符串来分隔元素,默认使用逗号:

1
2
3
4
5
6
var a = [1, 2, 3]; // Create a new array with these three elements
a.join(); // => "1,2,3"
a.join(" "); // => "1 2 3"
a.join(""); // => "123"
var b = new Array(10); // An array of length 10 with no elements
b.join('-') // => '---------': a string of 9 hyphens

join方法和split方法刚好相反。

reverse

reverse方法原地反转数组的顺序,并返回已反转的数组。

sort

sort原地排序数组元素,并返回已排序的数组。在不带参数的情况下,sort按字母顺序对数组元素进行排序,如果需要的话,将数组元素临时转换为字符串来进行比较。如果数组包含undefined元素,它们被移到数组后面。

要将数组按其它顺序排序,必须传递一个比较函数作为sort的参数。这个函数决定它的两个参数中的哪个应该先出现在数组中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var a = [33, 4, 1111, 222];
a.sort(); // Alphabetical order: 1111, 222, 33, 4
a.sort(function(a,b) { // Numerical order: 4, 33, 222, 1111
    return a-b;
});
a.sort(function(a,b) {return b-a}); // Reverse numerical order

a = ['ant', 'Bug', 'cat', 'Dog']
a.sort(); // case-sensitive sort: ['Bug','Dog','ant',cat']
a.sort(function(s,t) { // Case-insensitive sort
    var a = s.toLowerCase();
    var b = t.toLowerCase();
    if (a < b) return -1;
    if (a > b) return 1;
    return 0;
}); // => ['ant','Bug','cat','Dog']

concat

concat方法连接调用它的数组元素和它的参数,并返回一个新数组。如果参数是一个数组,那么连接的是数组元素,而不是数组本身,但是concat只处理最外层数组:

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

slice

语法如下:

1
2
Array.slice(start, end) // 返回包含[start, end)元素的数组
Array.slice(start) // 等价于Array.slice(start, Array.length)

如果参数为负数,它表示是数组倒数第几个元素:

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

a.slice(3); // Returns [4,5]
a.slice(1,-1); // Returns [2,3,4]
a.slice(-3,-2); // Returns [3]

splice

splice方法是用于插入或删除数组元素的通用方法,它会修改数组。splice第一个参数指定开始插入或删除的数组位置。第二个参数指定应该从数组中删除的元素个数。如果省略了第二个参数,那么从开始元素到数组末尾的所有数组元素都将被删除。splice返回删除元素的数组,如果没有删除元素,返回空数组:

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

splice的前两个参数指定要删除的数组元素。它们后面可以跟任意数量的附加参数,这些参数指定要插入到数组中的元素,从第一个参数指定的位置开始:

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

push和pop

pushpop允许你以栈的方式处理数组。push将一个或多个新元素追加到数组的末尾,并返回数组的新长度。pop删除数组最后一个元素,递减数组长度,并返回删除的值。这两个方法直接修改数组。

unshift和shift

unshift在开头添加一个或多个元素,将现有的数组元素移动到更高的索引位置,并返回数组的新长度。shift删除并返回数组的第一个元素,将所有后续元素向下移动一个位置:

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

多个参数调用unshift时,一次性插入所有的参数。

toString和toLocaleString

数组的toString方法将每个元素转换为一个字符串,并用逗号连接这些字符串:

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

数组的join方法不带参数时返回的字符串和toString方法相同。

toLocaleStringtoString的本地化版本。它通过调用元素的toLocaleString方法将每个元素转换为字符串,然后使用本地化的分隔符字符串连接结果字符串。

ES5数组方法

首先,大多数方法接受一个函数作为它的第一个参数,并为每个数组元素调用一次这个方法。如果是稀疏数组,不会为不存在的元素调用函数。大多数情况下,使用3个参数调用你提供的函数:数组元素的值,数组元素的索引和数组本身。通常你只需要第一个参数的值。

大多数接受函数作为第一个参数的ES5数组方法有一个可选的第二个参数。如果指定,则函数被作为第二个参数的方法调用。ES5数组方法不会修改调用它的数组。

forEach

语法如下:

1
a.forEach(fn[, o])

forEach遍历数组,并为每个元素调用指定的函数。forEach使用3个参数调用函数:数组元素的值、数组元素的索引和数组本身:

1
2
3
4
5
6
7
8
var data = [1,2,3,4,5]; // An array to sum
// Compute the sum of the array elements
var sum = 0; // Start at 0
data.forEach(function(value) { sum += value; }); // Add each value to sum
sum // => 15
// Now increment each array element
data.forEach(function(v, i, a) { a[i] = v + 1; });
data // => [2,3,4,5,6]

forEach不能中止循环。如果需要提前终止,可以抛出异常:

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

map

语法如下:

1
a.map(fn)

map方法将数组的每个元素传递给指定的函数,并返回一个包含该函数返回值的新数组:

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

传递给map的方法应该返回一个值。如果数组是稀疏的,返回的数组也是稀疏的。

filter

语法如下:

1
a.filter(fn)

filter方法返回数组元素子集的数组。传递给filter的函数应该是谓词。如果谓词返回值为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
2
var dense = sparse.filter(function() { return true; });
a = a.filter(function(x) { return x !== undefined && x != null; });

every和some

语法如下:

1
2
a.every(fn)
a.some(fn)

every方法类似数学符号: 当且仅当谓词函数对数组所有元素返回true时,它才返回true。

1
2
3
a = [1,2,3,4,5];
a.every(function(x) { return x < 10; }) // => true: all values < 10.
a.every(function(x) { return x % 2 === 0; }) // => false: not all values even.

some方法类似数学符号:如果存在一个元素使谓词函数返回true,则返回true。

1
2
3
a = [1,2,3,4,5];
a.some(function(x) { return x%2===0; }) // => true a has some even numbers.
a.some(isNaN) // => false: a has no non-numbers.

everysome都是一旦能确定返回值时就停止迭代。根据数学惯例,当在空数组上调用时,every返回true,some返回false。

reduce和reduceRight

语法如下:

1
2
a.reduce(fn[, initial]) // initial为初始值
a.reduceRight(fn[, initial])

reducereduceRight 方法使用指定函数计算数组元素:

1
2
3
4
var a = [1,2,3,4,5]
var sum = a.reduce(function(x,y) { return x+y }, 0); // Sum of values
var product = a.reduce(function(x,y) { return x*y }, 1); // Product of values
var max = a.reduce(function(x,y) { return (x>y)?x:y; }); // Largest value

reducereduceRight方法中,传递给fn的第一个参数是已经累计的结果,数组元素的值、数组元素的索引和数组本身紧跟第一个参数。第一次调用fn时,第一个参数为初始值。后续调用的第一个参数是前一个调用的返回值。

当没有初始值调用reduce时,初始值为数组的第一个元素。没有初始值时在空数组上调用reduce会导致TypeError。

以下2中情况,reduce不会调用fn,而是直接返回那个值:

  • 只有一个元素数组,没有初始值
  • 空数组和初始值

reduceRight的工作原理与reduce类似,只是它从高到低处理数组:

1
2
3
4
5
var a = [2, 3, 4]
// Compute 2^(3^4). Exponentiation has right-to-left precedence
var big = a.reduceRight(function(accumulator,value) {
    return Math.pow(value,accumulator);
});

reducereduceRight不支持方法调用。如果想要调用方法,可以使用Function.bind

reducereduceRight不仅仅用于数学计算,考虑上一章的union函数:

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

当两个对象具有相同名称的属性时,union函数从第一个参数使用该属性的值。因此reducereduceRight可能会返回不同的结果:

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.reduceRight(union); // {x:1, y:2, z:3, a:3}

indexOf和lastIndexOf

语法如下:

1
2
a.indexOf(value[, start]) // 从头到尾搜索数组
a.lastIndexOf(value[, start]) // 与indexOf相反

返回找到的第一个元素的索引,如果没找到则返回-1:

1
2
3
4
a = [0,1,2,1,0];
a.indexOf(1) // => 1: a[1] is 1
a.lastIndexOf(1) // => 3: a[3] is 1
a.indexOf(3) // => -1: no element has value 3

第二可选参数指定搜索的起始位置,它可以是负值,像splice方法一样当作与数组末尾的偏移量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Find all occurrences of a value x in an array a and return an array
// of matching indexes
function findall(a, x) {
    var results = [], // The array of indexes we'll return
        len = a.length, // The length of the array to be searched
        pos = 0; // The position to search from
    while(pos < len) { // While more elements to search...
        pos = a.indexOf(x, pos); // Search
        if (pos === -1) break; // If nothing found, we're done.
        results.push(pos); // Otherwise, store index in array
        pos = pos + 1; // And start next search at next element
    }
    return results; // Return array of indexes
}

数组类型

在ES5中,可以使用Array.isArray来判断一个对象是否是数组:

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

在ES5之前,区分数组和非数组对象是非常困难的。除了函数,typeof一个对象返回objectinstanceof在简单的情况下工作:

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

解决方案是检查对象的class属性。对于数组,这个属性总是有值“Array”,因此我们可以这样在ES3中这样编写isArray函数:

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

类数组对象

JavaScript数组有一些其他对象没有的特殊特性:

  • 当向数组添加新元素时,length属性将自动更新。
  • length设置为较小的值会截断数组。
  • 数组从Array.prototype继承了有用的方法。
  • 数组具有类属性“Array“。

事实证明,许多数组算法对类数组对象的工作效果与对数组的工作效果一样好。arguments对象是一个类数组对象,document.getElementsByTagName方法返回一个类数组对象。以下是一个测试像数组一样工作的对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Determine if o is an array-like object.
// Strings and functions have numeric length properties, but are
// excluded by the typeof test. In client-side JavaScript, DOM text
// nodes have a numeric length property, and may need to be excluded
// with an additional o.nodeType != 3 test.
function isArrayLike(o) {
    if (o && // o is not null, undefined, etc.
        typeof o === "object" && // o is an object
        isFinite(o.length) && // o.length is a finite number
        o.length >= 0 && // o.length is non-negative
        o.length===Math.floor(o.length) && // o.length is an integer
        o.length < 4294967296) // o.length < 2^32
        return true; // Then o is array-like
    else
        return false; // Otherwise it is not
}

JavaScript数组方法被特意定义为通用的,以便数组和类数组对象都能正确工作。在ES5中,所有数组方法都是通用的。在ES3中,除了toStringtoLocaleString之外的所有方法都是通用的。因为类数组对象不继承Array.prototype,你不能直接调用数组方法。但你可以使用Function.call函数间接调用它们:

1
2
3
4
5
6
var a = {"0":"a", "1":"b", "2":"c", length:3}; // An array-like object
Array.prototype.join.call(a, "+") // => "a+b+c"
Array.prototype.slice.call(a, 0) // => ["a","b","c"]: true array copy
Array.prototype.map.call(a, function(x) {
    return x.toUpperCase();
}) // => ["A","B","C"]:

Firefox很早就在数组构造函数上定义了函数,上面的例子可以这样重写:

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

这些数组方法的静态函数版本在处理类数组对象时非常有用,但是由于它们是非标准的,所以不能指望在所有浏览器中都定义它们。在使用之前,你可以这样确保所需的函数存在:

1
2
3
4
5
6
7
8
9
Array.join = Array.join || function(a,sep) {
    return Array.prototype.join.call(a,sep);
};
Array.slice = Array.slice || function(a,from,to) {
    return Array.prototype.slice.call(a,from,to);
};
Array.map = Array.map || function(a, f, thisArg) {
    return Array.prototype.map.call(a, f, thisArg);
}

字符串作为类数组

在ES5(以及在ES5之前的许多浏览器实现)中,字符串表现得像只读数组。你可以使用[],而不是charAt方法访问单个字符:

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

字符串表现得像数组也意味着,我们可以对它们使用通用数组方法:

1
2
3
4
5
6
s = "JavaScript"
Array.prototype.join.call(s, " ") // => "J a v a S c r i p t"
Array.prototype.filter.call(s, // Filter the characters of the string
    function(x) {
        return x.match(/aeiou/); // Only match nonvowels
    }).join("") // => "JvScrpt"

字符串是不可变的值,所以当它们被当作数组时,它们是只读数组。数组方法如pushsortreversesplice,会原地修改数组,不能在字符串上工作。尝试使用数组方法修改字符串不会导致错误,只是默默地失败。