七.JavaScript数组

279 阅读16分钟

数组是值的有序集合。每个值叫做一个元素,而每个元素在数组中有一个位置,以数字表示,称为索引。 JavaScript数组是JavaScript对象的特殊形式,数组索引实际上和碰巧是整数的属性名差不多。 通常,数组的实现是经过优化的,用数字索引来访问数组元素一般来说比访问常规的对象属性要快很多。

1. 创建数组

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

var empty = []; //没有元素的数组
var primes = [2, 3, 5, 7, 11]; //有5个数值的数组
var misc = [1.1, true, "a"]; //3个不同类型的元素和结尾的逗号
var base = 1024;
var table = [base, 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()是创建数组的另一种方法。

var a = new Array(); //该方法创建一个没有任何元素的空数组,等同于数组直接量[]。

// 创建指定长度的数组。当预先知道所需元素个数时,这种形式的Array()构造函数可以用来预分配一个数组空间。
// 注意,数组中没有存储值,甚至数组的索引属性“0”、“1”等还未定义。
var a = new Array(10);
var a = new Array(5, 4, 3, 2, 1, "testing,testing");

2. 数组元素的读和写

使用[]操作符来访问数组中的一个元素。位于方括号的左边的是数组的引用

var a = ["world"]; //从一个元素的数组开始
var value = a[0]; //读第0个元素
a[1] = 3.14; //写第1个元素
i = 2;
a[i] = 3; //写第2个元素
a[i + 1] = "hello"; //写第3个元素
a[a[i]] = a[0]; //读第0个和第2个元素,写第3个元素

数组是对象的特殊形式。使用方括号访问数组元素就像用方括号访问对象的属性一样。JavaScript将指定的数字索引值转换成字符串——索引值1变成“1”——然后将其作为属性名来使用。

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

3. 稀疏数组

稀疏数组就是包含从0开始的不连续索引的数组。通常,数组的length属性值代表数组中元素的个数。如果数组是稀疏的,length属性值大于元素的个数。

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

image.png

4. 数组长度

每个数组有一个length属性,就是这个属性使其区别于常规的JavaScript对象。

数组长度保证大于它每个元素的索引值,为了维持此规则不变化:

  • 如果为一个数组元素赋值,它的索引i大于或等于现有数组的长度时,length属性的值将设置为i+1。
  • 设置length属性为一个小于当前长度的非负整数n时,当前数组中那些索引值大于或等于n的元素将从中删除:
a = [1, 2, 3, 4, 5]; //从5个元素的数组开始
a.length = 3; //现在a为[1,2,3]
a.length = 0; //删除所有的元素。a为[]
a.length = 5; //长度为5,但是没有元素,就像new Array(5)

可以用Object.defineProperty()让数组的length属性变成只读的

image.png

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

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

  • 使用push()方法在数组末尾增加一个或多个元素,在数组尾部压入一个元素与给数组a[a.length]赋值是一样的。
  • 可以使用unshift()方法在数组的首部插入一个元素,并且将其他元素依次移到更高的索引处。
  • 可以像删除对象属性一样使用delete运算符来删除数组元素:
a = [1, 2, 3];
delete a[1]; //a在索引1的位置不再有元素
1 in a; //=>false:数组索引1并未在数组中定义
a.length; //=>3:delete操作并不影响数组长度

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

  • 数组有pop()方法(它和push()一起使用),后者一次使length减少长度1并返回被删除元素的值。
  • shift()方法(它和unshift()一起使用),从数组头部删除一个元素。和delete不同的是shift()方法将所有元素下移到比当前索引低1的地方。
  • splice()是一个通用的方法来插入、删除或替换数组元素。它会根据需要修改length属性并移动元素到更高或较低的索引处。

6. 数组遍历

①for循环遍历,数组的长度应该只查询一次而非每次循环都要查询:

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

②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;
}

③遍历数组元素的新方法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

7. 多维数组

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

使用二维数组作为一个九九乘法表:

//创建一个多维数组
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;
  }
}
//使用多维数组来计算(查询)5*7
var product = table[5][7]; //35

8. Array.prototype中的数组方法

image.png

8.1 join()

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

var a = [1, 2, 3]; //创建一个包含三个元素的数组
a.join(); //=>"1,2,3"
a; //[1,2,3],不改变原数组
a.join(" "); //=>"1 2 3"
a.join(""); //=>"123"
var b = new Array(10); //长度为10的空数组
b.join("-"); //=>'---------':9个连字号组成的字符串

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

8.2 reverse()

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

image.png

8.3 sort()

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

image.png

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

sort()方法改变了原数组

  • 给sort()方法传递一个比较函数。该函数决定了它的两个参数在排好序的数组中的先后顺序。
  • 假设第一个参数应该在前,比较函数应该返回一个小于0的数值。(<0 可看作false,说明两个值不用交换位置
  • 反之,假设第一个参数应该在后,函数应该返回一个大于0的数值。(>0看作true,对两个值执行一次位置交换
  • 并且,假设两个值相等(也就是说,它们的顺序无关紧要),函数应该返回0。
var a = [33, 4, 1111, 222];
a.sort(); //字母表顺序:1111,222,33,4
a.sort(function(a, b) {
  //数值顺序:4,33,222,1111
  return a - b; //根据顺序,返回负数、0、正数
});
a.sort(function(a, b) {
  return b - a;
}); //数值大小相反的顺序

也许需要对一个字符串数组执行不区分大小写的字母表排序

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; // 不交换a,b位置, 说明是递增排列的
  if (a > b) return 1; // 看作true,调用一次交换
  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]
// concat()不会递归扁平化`数组的数组`
a.concat(4, [5, [6, 7]]); //返回[1,2,3,4,5,[6,7]]
a; // [1,2,3] concat()也不会修改调用的数组

8.5 sice()

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]
a; // [1,2,3,4,5] 不修改原数组

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()能够用JavaScript数组实现先进后出的栈。例如:
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(); //stack:[] 返回1

8.8 unshift()和shift()

  • unshift()在数组的头部添加一个或多个元素,并将已存在的元素移动到更高索引的位置来获得足够的空间,最后返回数组新的长度
  • 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()

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

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

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

toLocaleString()是toString()方法的本地化版本。

9. ES5一些高级数组方法(接收函数作为参数)

  • 大多数方法的第一个参数接收一个函数,并且对数组的每个元素(或一些元素)调用一次该函数。
  • 在大多数情况下,调用提供的函数使用三个参数:数组元素、元素的索引和数组本身。
  • 如果有第二个参数,则调用的函数被看做是第二个参数的方法。也就是说,在调用函数时传递进去的第二个参数作为它的this关键字的值来使用。

9.1 forEach()

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

如上所述,传递的函数作为forEach()的第一个参数。然后forEach()使用三个参数调用该函数:数组元素、元素的索引和数组本身。如果只关心数组元素的值,可以编写只有一个参数的函数——额外的参数将忽略:

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语句。

image.png

如果要提前终止,必须把forEach()方法放在一个try块中,并能抛出一个异常。如果forEach()调用的函数fn()抛出fn.break异常,循环会提前终止:

image.png

9.2 map()

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

a = [1, 2, 3];
b = a.map(function(x) {
  return x * x;
}); //b[1,4,9]
a; // [1,2,3]不改变原数组

传递给map()的函数的调用方式和传递给forEach()的函数的调用方式一样。但传递给map()的函数应该有返回值。注意,map()返回的是新数组:它不修改调用的数组

9.3 filter()

fliter()方法返回的数组元素是调用的数组的一个子集

  • 传递的函数是用来逻辑判定的:该函数返回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]
a; //[5,4,3,2,1]不改变原数组

9.4 every()和some()

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

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:

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

注意,一旦every()和some()确认该返回什么值它们就会停止遍历数组元素。

注意,根据数学上的惯例,在空数组上调用时,every()返回true,some()返回false。

image.png

9.5 reduce()和reduceRight()

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

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()使用的函数与forEach()和map()使用的函数不同.

  • 数组元素、元素的索引和数组本身将作为第2~4个参数传递给化简函数。
  • 第一个参数x是到目前为止的化简操作累积的结果。
  • 第一次调用化简函数时,它的第一个参数x是一个初始值,它就是传递给reduce()的第二个初始值参数。在接下来的调用中,这个值就是上一次化简函数的返回值。
  • 化简函数的第二个参数y是依次遍历取到的数组元素

在上面的第一个例子中,第一次调用化简函数时的参数是0和1。将两者相加并返回1。再次调用时的参数是1和2,它返回3。然后它计算3+3=6、6+4=10,最后计算10+5=15。最后的值是15,reduce()返回这个值。

当不指定初始值调用reduce()时,它将使用数组的第一个元素作为其初始值。这意味着第一次调用化简函数就使用了第一个和第二个数组元素作为其第一个和第二个参数。

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

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

注意,reduce()和reduceRight()都能接收一个可选的参数,它指定了化简函数调用时的this关键字的值。可选的初始值参数仍然需要占一个位置。

使用reduce()来,计算任意数目的对象的“并集”:

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

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

image.png

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()从末尾开始搜索。
  • 第二个参数也可以是负数,它代表相对数组末尾的偏移量,例如,-1指定数组的最后一个元素。

运用indexOf()的第二个参数来查找除了第一个以外匹配的值。

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; //返回包含索引的数组
}

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

10 数组类型

给定一个未知的对象,判定它是否为数组。 typeof操作符在这里帮不上忙:对数组它返回“对象”(并且对于除了函数以外的所有对象都是如此)

①使用Array.isArray()函数

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

②检查对象的类属性

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

11. 类数组对象

常常把拥有一个数值length属性和对应非负整数属性的对象看做一种类型的数组。

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

image.png

12 可看作数组的字符串

字符串的行为类似于数组的事实使得通用的数组方法可以应用到字符串上。例如:

image.png

请记住,字符串是不可变值,故当把它们作为数组看待时,它们是只读的。如push()、sort()、reverse()和splice()等数组方法会修改数组,它们在字符串上是无效的。