跳到主要内容

3. 数组

创建和初始化数组

let daysOfWeek = new Array(); // 使用new关键字,声明并初始化一个数组
daysOfWeek = new Array(7); // 创建一个制定长度的数组
daysOfWeek = new Array(
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
); // 将数组元素作为参数传递给数组的构造器

let daysOfWeek = []; // 直接用中括号([])创建数组
let daysOfWeek = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];

console.log(daysOfWeek.length); // 使用length获取数组中元素的数量

访问元素和迭代数组

for (let i = 0; i < daysOfWeek.length; i++) {
console.log(daysOfWeek[i]);
}

求斐波那契数列的前 20 个数。已知斐波那契数列中的前两项是 1,从第三项开始,每一项都等于前两项之和。

const fibonacci = []; // {1}
fibonacci[1] = 1; // {2}
fibonacci[2] = 1; // {3}

for (let i = 3; i < 20; i++) {
fibonacci[i] = fibonacci[i - 1] + fibonacci[i - 2]; // {4}
}
for (let i = 1; i < fibonacci.length; i++) {
// {5}
console.log(fibonacci[i]); // {6}
}

数组的方法

  • push:在数组的末尾添加元素
    • numbers.push(11); numbers.push(12, 13);
  • unshift:在数组的开头添加元素
    • numbers.unshift(-2); numbers.unshift(-4, -3);
  • pop:在数组的末尾删除元素
    • numbers.pop();
  • shift:在数组的开头删除元素
    • numbers.shift();
  • splice:在数组的任意位置添加或删除元素
    • numbers.splice(5,3); 删除了从数组索引 5 开始的 3 个元素
    • numbers.splice(5,3); 在数组索引 5 之后插入 2,3,4 这 3 个元素

二维数组和多维数组

let averageTemp = [];
averageTemp[0] = [72, 75, 79, 79, 81, 81];
averageTemp[1] = [81, 79, 75, 75, 73, 73];

// 迭代二维数组中的元素
function printMatrix(myMatrix) {
for (let i = 0; i < myMatrix.length; i++) {
for (let j = 0; j < myMatrix[i].length; j++) {
console.log(myMatrix[i][j]);
}
}
}

printMatrix(averageTemp);

创建一个 3×3×3 的矩阵,每一格里包含矩阵的 i(行)、j(列)及 z(深度)之和。

const matrix3x3x3 = [];
for (let i = 0; i < 3; i++) {
matrix3x3x3[i] = []; // 我们需要初始化每个数组
for (let j = 0; j < 3; j++) {
matrix3x3x3[i][j] = [];
for (let z = 0; z < 3; z++) {
matrix3x3x3[i][j][z] = i + j + z;
}
}
}

输出上面矩阵中的内容

for (let i = 0; i < matrix3x3x3.length; i++) {
for (let j = 0; j < matrix3x3x3[i].length; j++) {
for (let z = 0; z < matrix3x3x3[i][j].length; z++) {
console.log(matrix3x3x3[i][j][z]);
}
}
}

常见数组方法参考

image

数组合并

concat 方法可以向一个数组传递数组、对象或是元素。数组会按照该方法传入的参数顺序连接指定数组

const zero = 0;
const positiveNumbers = [1, 2, 3];
const negativeNumbers = [-3, -2, -1];
let numbers = negativeNumbers.concat(zero, positiveNumbers); // -3、-2、-1、0、1、2、3
let numbers = negativeNumbers.concat(zero, 1, 1, 1); // -3、-2、-1、0、1、1、1

迭代器函数

function isEven(x) {
// 如果x是2 的倍数,就返回true
console.log(x);
return x % 2 === 0 ? true : false;
}
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];

// 使用ES6改写方法
const isEven2 = (x) => x % 2 === 0;

every

every 方法会迭代数组中的每个元素,直到返回 false

numbers.every(isEven);

some

some 方法跟 every 的行为刚好相反,它会迭代数组中的每个元素,直到函数返回 false

numbers.some(isEven);

forEach

forEach 方法会迭代整个数组,结果与 for 循环相同

numbers.forEach((x) => console.log(x % 2 === 0));

map 和 filter 方法

map 方法会迭代整个数组,返回一个新数组,里面保存的是传入 map 方法的运行结果

const myMap = numbers.map(isEven); // [false, true, false, true, false, true,false, true, false, true, false, true, false, true, false]

filter 方法会迭代整个数组,返回一个新数组,里面保存的是传入 filter 方法的运行返回为 true 的元素

const evenNumbers = numbers.filter(isEven); // [2,4, 6, 8, 10, 12, 14]

reduce 方法

reduce 方法接收一个有如下四个参数的函数:previousValue、currentValue、index 和 array。因为 index 和 array 是可选的参数,所以如果用不到它们的话,可以不传。这个函数会返回一个将被叠加到累加器的值,reduce 方法停止执行后会返回这个累加器。如果要对一个数组中的所有元素求和,这就很有用。

numbers.reduce((previous, current) => previous + current);

ECMAScript 6 和数组的新功能

ES2015 和 ES2016 新增的数组方法

image

使用 for...of 循环迭代

for (const n of numbers) {
console.log(n % 2 === 0 ? "even" : "odd");
}

使用@@iterator 对象

ES2015 还为 Array 类增加了一个@@iterator 属性,需要通过 Symbol.iterator 来访问

let iterator = numbers[Symbol.iterator]();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
console.log(iterator.next().value); // 4
console.log(iterator.next().value); // 5

然后,不断调用迭代器的 next 方法,就能依次得到数组中的值。numbers 数组中有 15 个值,因此需要调用 15 次 iterator.next().value。

我们可以用下面的代码来输出 numbers 数组中的 15 个值。

iterator = numbers[Symbol.iterator]();
for (const n of iterator) {
console.log(n);
}

数组中的所有值都迭代完之后,iterator.next().value 会返回 undefined。

数组的 entries、keys 和 values 方法

entries 方法返回包含键值对的@@iterator,下面是使用该方法的代码示例
let aEntries = numbers.entries(); // 得到键值对的迭代器
console.log(aEntries.next().value); // [0, 1] - 位置0 的值为1
console.log(aEntries.next().value); // [1, 2] - 位置1 的值为2
console.log(aEntries.next().value); // [2, 3] - 位置2 的值为3

numbers 数组中都是数,key 是数组中的位置,value 是保存在数组索引的值。我们也可以使用下面的代码。

aEntries = numbers.entries();
for (const n of aEntries) {
console.log(n);
}

使用集合、字典、散列表等数据结构时,能够取出键值对是很有用的。

keys 方法返回包含数组索引的@@iterator,下面是使用该方法的代码示例
    const aKeys = numbers.keys(); // 得到数组索引的迭代器
console.log(aKeys.next()); // {value: 0, done: false }
console.log(aKeys.next()); // {value: 1, done: false }
console.log(aKeys.next()); // {value: 2, done: false }
```

keys 方法会返回 numbers 数组的索引。一旦没有可迭代的值,aKeys.next()就会返回一个 value 属性为 undefined、done 属性为 true 的对象。如果 done 属性的值为 false,就意味着还有可迭代的值。

values 方法返回的@@iterator 则包含数组的值。使用这个方法的代码示例如下
const aValues = numbers.values();
console.log(aValues.next()); // {value: 1, done: false }
console.log(aValues.next()); // {value: 2, done: false }
console.log(aValues.next()); // {value: 3, done: false }

from 方法

Array.from 方法根据已有的数组创建一个新数组。比如,要复制 numbers 数组,可以如下这样做。

let numbers2 = Array.from(numbers);
// 还可以传入一个用来过滤值的函数,例子如下。
let evens = Array.from(numbers, (x) => x % 2 == 0);

使用 Array.of 方法

Array.of 方法根据传入的参数创建一个新数组。以下面的代码为例。

let numbers3 = Array.of(1);
let numbers4 = Array.of(1, 2, 3, 4, 5, 6);

// 它和下面这段代码的效果一样。
let numbers3 = [1];
let numbers4 = [1, 2, 3, 4, 5, 6];

// 我们也可以用该方法复制已有的数组,
let numbersCopy = Array.of(...numbers4);

使用 fill 方法

fill 方法用静态值填充数组。以下面的代码为例。

let numbersCopy = Array.of(1, 2, 3, 4, 5, 6); // numbersCopy数组的length是6,也就是有6个位置

numbersCopy.fill(0); // numbersCopy数组所有位置上的值都会变成0([0, 0, 0, 0, 0, 0])。

// 指定开始填充的索引,如下所示。
numbersCopy.fill(2, 1); // 数组中从1开始的所有位置上的值都是2([0, 2, 2, 2, 2, 2])。

// 同时制定开始填充和结束填充的索引
numbersCopy.fill(1, 3, 5); // 把1填充到数组索引3到5的位置(包括3,不包括5),得到的数组为[0, 2, 2, 1,1, 2]。

创建数组并初始化值的时候,fill 方法非常好用,就像下面这样。

let ones = Array(6).fill(1); // 创建了一个长度为6、所有值都是1的数组([1, 1, 1, 1, 1, 1])。

使用 copyWithin 方法

copyWithin 方法复制数组中的一系列元素到同一数组指定的起始位置。看看下面这个例子。

let copyArray = [1, 2, 3, 4, 5, 6];
// 假如我们想把4、5、6三个值复制到数组前三个位置,得到[4, 5, 6, 4, 5, 6]这个数组,可以用下面的代码达到目的。
copyArray.copyWithin(0, 3);
// 假如我们想把4、5两个值(在位置3和4上)复制到位置1和2,可以这样做:
copyArray = [1, 2, 3, 4, 5, 6];
copyArray.copyWithin(1, 3, 5); // 这种情况下,会把从位置3开始到位置5结束(包括3,不包括5)的元素复制到位置1,结果是得到数组[1, 4, 5, 4, 5, 6]。

排序元素

  • reverse:数组反转
  • sort:数组排序
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
numbers.reverse(); // [15, 14, 13, 12, 11, 10, 9, 8,7, 6, 5, 4, 3, 2, 1]
numbers.sort(); // [1, 10, 11, 12, 13, 14, 15, 2, 3,4, 5, 6, 7, 8, 9]

看起来不大对,是吧?这是因为 sort 方法在对数组做排序时,把元素默认成字符串进行相互比较。

我们可以传入自己写的比较函数。因为数组里都是数,所以可以像下面这样写。

numbers.sort((a, b) => a - b);

在 b 大于 a 时,这段代码会返回负数,反之则返回正数。如果相等的话,就会返回 0。也就是说返回的是负数,就说明 a 比 b 小,这样 sort 就能根据返回值的情况对数组进行排序。

之前的代码也可以表示成如下这样,会更清晰一些。

function compare(a, b) {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
// a必须等于b
return 0;
}
numbers.sort(compare);

这是因为 JavaScript 的 sort 方法接收 compareFunction 作为参数,然后 sort 会用它排序数组。在这个例子里,我们声明了一个用来比较数组元素的函数,使数组按升序排序。

自定义排序

我们可以对任何对象类型的数组排序,也可以创建 compareFunction 来比较元素。例如,对象 Person 有名字和年龄属性,我们希望根据年龄排序,就可以这么写。

const friends = [
{ name: "John", age: 30 },
{ name: "Ana", age: 20 },
{ name: "Chris", age: 25 }, // ES2017 允许存在尾逗号
];
function comparePerson(a, b) {
if (a.age < b.age) {
return -1;
}
if (a.age > b.age) {
return 1;
}
return 0;
}
console.log(friends.sort(comparePerson));

在这个例子里,最后会输出 Ana(20), Chris(25), John(30)。

字符串排序

假如有这样一个数组。你猜会输出什么?

let names = ["Ana", "ana", "john", "John"];
console.log(names.sort()); // ["Ana", "John", "ana", "john"]

既然 a 在字母表里排第一位,为何 ana 却排在了 John 之后呢?这是因为 JavaScript 在做字符比较的时候,是根据字符对应的 ASCII 值来比较的。例如,A、J、a、j 对应的 ASCII 值分别是 65、74、97、106。

虽然 a 在字母表里是最靠前的,但 J 的 ASCII 值比 a 的小,所以排在了 a 前面。

现在,如果给 sort 传入一个忽略大小写的比较函数,将输出["Ana", "ana", "John","john"]

names = ["Ana", "ana", "john", "John"]; // 重置数组的初始状态
console.log(
names.sort((a, b) => {
if (a.toLowerCase() < b.toLowerCase()) {
return -1;
}
if (a.toLowerCase() > b.toLowerCase()) {
return 1;
}
return 0;
})
); // ['Ana', 'ana', 'john', 'John']

在这种情况下,sort 函数不会有任何作用。它会按照现在的大小写字母顺序排序。

如果希望小写字母排在前面,那么需要使用 localeCompare 方法。

names.sort((a, b) => a.localeCompare(b));

输出结果将是["ana", "Ana", "john", "John"]

假如对带有重音符号的字符做排序的话,也可以用 localeCompare 来实现。

const names2 = ["Maève", "Maeve"];
console.log(names2.sort((a, b) => a.localeCompare(b))); // ["Maeve", "Maève"];

搜索

  • indexOf 方法返回与参数匹配的第一个元素的索引;
  • lastIndexOf 返回与参数匹配的最后一个元素的索引
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
console.log(numbers.indexOf(10)); // 9
console.log(numbers.indexOf(100)); // -1 (100不在数组中)

numbers.push(10);
console.log(numbers.lastIndexOf(10)); // 15,此时数组中元素为[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10]
console.log(numbers.lastIndexOf(100)); // -1(100不在数组中)

ECMAScript 2015——find 和 findIndex 方法

find 和 findIndex 方法接收一个回调函数,搜索一个满足回调函数条件的值。

find 和 findIndex 的不同之处在于,find 方法返回第一个满足条件的值,findIndex 方法则返回这个值在数组里的索引。如果没有满足条件的值,find 会返回 undefined,而 findIndex 返回-1。

// 从数组里找一个13的倍数。
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
function multipleOf13(element, index, array) {
return element % 13 == 0;
}
console.log(numbers.find(multipleOf13)); // 13
console.log(numbers.findIndex(multipleOf13)); // 12

ECMAScript 7——使用 includes 方法

如果数组里存在某个元素,includes 方法会返回 true,否则返回 false。使用 includes 方法的例子如下。

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
console.log(numbers.includes(15)); // true
console.log(numbers.includes(20)); // false

如果给 includes 方法传入一个起始索引,搜索会从索引指定的位置开始。

let numbers2 = [7, 6, 5, 4, 3, 2, 1];
console.log(numbers2.includes(4, 5)); // false 数组索引5之后的元素不包含4。

输出数组为字符串

如果想把数组里所有元素输出为一个字符串,可以用 toString 方法。

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
console.log(numbers.toString()); // 1、2、3、4、5、6、7、8、9、10、11、12、13、14、15

如果想用一个不同的分隔符(比如-)把元素隔开,可以用 join 方法。

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
const numbersString = numbers.join("-");
console.log(numbersString); // 1-2-3-4-5-6-7-8-9-10-11-12-13-14-15

类型数组

与 C 和 Java 等其他语言不同,JavaScript 数组不是强类型的,因此它可以存储任意类型的数据。

类型数组则用于存储单一类型的数据。它的语法是 let myArray = new TypedArray (length),其中 TypedArray 需替换为下表所列之一。

image

let length = 5;
let int16 = new Int16Array(length);

let array16 = [];
array16.length = length;

for (let i = 0; i < length; i++) {
int16[i] = i + 1;
}
console.log(int16);

image

使用 WebGL API、进行位操作、处理文件和图像时,类型数组都可以大展拳脚。它用起来和普通数组毫无二致,本章所学的数组方法和功能都可以用于类型数组。https://www.html5rocks.com/en/tutorials/webgl/typed_arrays/ 是一个很好的教程,讲解了如何使用类型数组处理二进制数据,以及它在实际项目中的应用。

TypeScript 中的数组

TypeScript 会在编译时进行类型检测,来确保只对所有值都属于相同数据类型的数组进行操作。

如果检查下面这段代码,会发现它和本章前几节声明的 numbers 数组是一样的。

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

根据类型推断,TypeScript 能够理解 numbers 数组的声明和 const numbers: number[]是一样的。出于这个原因,如果我们在声明时给变量赋了初始值,就不需要每次都显式声明变量的类型了。

回到对 friends 数组的排序示例,我们可以用 TypeScript 将代码重构成如下这样。

interface Person {
name: string;
age: number;
}

// const friends: {name: string, age: number}[];
const friends = [
{ name: "John", age: 30 },
{ name: "Ana", age: 20 },
{ name: "Chris", age: 25 },
];

function comparePerson(a: Person, b: Person) {
// comparePerson函数的内容
}

通过声明 Person 接口,我们确保了 comparePerson 函数只接收包含 name 和 age 属性的对象。friends 数组没有显式的类型,因此可以在本例中通过 const friends: Person[]显式声明它的类型。

总之,如果想用 TypeScript 给 JavaScript 变量设置类型,我们只需要使用 constlet variableName: <type>[],抑或像我们在第 1 章中学习到的,在使用.js 扩展名的文件时,在第一行添加注释 // @ts-check

在运行时,输出结果和使用纯 JavaScript 时是一样的。