本文是JavaScript基础教程中的第六章,在这一章中,我们将学习数组(也就是一种可以存储多个数据的容器),同时,我们还将学习操纵数组数据的多种方式。 在操纵数组数据的过程中,你将接触到有关对象的概念,比如属性、方法,此外,你还将接触到关于回调函数的概念。
什么是数组?
我们在变量一章中学习了变量,变量可以存储一个数据,比如:
var name = "张三";
但是,如果我们想要存储多个数据,比如存储一组水果的名字,那么我们就需要多个变量,比如:
var fruit1 = "apple";
var fruit2 = "banana";
var fruit3 = "orange";
var fruit4 = "pear";
这样做虽然可以实现我们的目的,但是却很麻烦,而且不利于管理。如果我们想要获取所有水果的名字,那么我们就需要写下面这样的代码:
console.log(fruit1);
console.log(fruit2);
console.log(fruit3);
console.log(fruit4);
这样做不仅麻烦(特别是当你有很多种水果的时候),而且容易出错:
如果我们想要添加一个水果,那么我们就需要添加一个变量,而且还要修改获取水果名字的代码。
如果我们想要删除一个水果,那么我们就需要删除一个变量,而且还要修改获取水果名字的代码。
如果我们想要修改一个水果的名字,那么我们就需要修改一个变量,而且还要修改获取水果名字的代码。
...
这样做显然不是一个好的选择,那么有没有更好的方法呢?当然有,那就是数组!
数组在编程中非常常用,从记录你的高考志愿,到统计微信好友的生日, 再到保存抖音视频的点赞数,都可以用数组来实现。那么什么是数组呢?
数组是一种数据类型,它可以存储多个相同或不同类型的数据,并且每个数据都有一个编号(索引),从0开始。
比如,你有一个数组叫做 fruits
,它里面存储了四种水果的名字:
var fruits = ["apple", "banana", "orange", "pear"];
看!我们只用一行代码就实现了我们想要的功能。
在这个数组中有四个元素(数据),分别是 "apple"
,"banana"
,"orange"
和 "pear"
。
每个元素都有一个索引,从0开始,也就是说 "apple"
的索引是 0,"banana"
的索引是 1,以此类推。
你可以通过索引来访问数组中的元素,就像下面这样:
console.log(fruits[0]); // 输出"apple"
console.log(fruits[3]); // 输出"pear"
但注意,索引不能超过数组的长度(元素的个数),否则会得到一个undefined
(未定义)的值。比如说:
console.log(fruits[4]); // 输出undefined
数组越界
这种情况也叫做数组越界,它是指访问数组的时候,下标的取值不在已定义好的数组的取值范围内,因此访问的是无法获取的内存地址。 这类错误也是编程中最常见的错误之一。
不同的编程语言对数组越界的处理方式也不同。有些语言会在编译或运行时检查数组边界,并抛出异常或错误,比如 Java,Python,C# 等。 有些语言则不检查数组边界,而是允许访问越界的内存地址,但可能导致程序崩溃或数据被破坏,比如 C,C++ 等。 还有些语言则会自动扩展数组的大小,以适应越界的访问,比如 JavaScript,Ruby 等。
JavaScript 中越界的特点是:当访问一个不存在的元素时,会返回 undefined 值,而不会报错。 当给一个不存在的元素赋值时,会自动扩展数组的长度,并用空位(empty)填充中间的元素。 空位只是表面上等于 undefined 值。空位可以通过 delete 操作符或者稀疏数组(sparse array)来产生。
例如:
var arr = [1, 2, 3];
console.log(arr[3]); // 输出 undefined
arr[5] = 6; // 给不存在的元素赋值
console.log(arr); // 输出 [1, 2, 3, 空属性 × 2, 6]
console.log(arr[4]); // ES6语法下,输出 undefined
console.log(arr.length); // 输出 6
可以看到,JavaScript 中越界的访问并不会导致程序出错,但会改变数组的长度和内容。 这可能会引起一些意想不到的结果或者难以发现的 bug。因此,在使用 JavaScript 数组时,还是要注意避免越界的情况。
怎么创建数组?
要使用JavaScript创建数组,我们必须用JavaScript引擎能听懂的语言,即使用规范合法的代码来告诉它我们想要创建什么样的数组。
虽然上面我们给出了一个例子,但我们还需要系统地学习一下! 接下来就让我们看看怎么告诉引擎我们想要创建什么样的数组吧!
创建数组有两种方法,最常用的一种是使用方括号 []
。
使用这种方法时,你需要在方括号里面写上你想要存储的元素,用逗号分隔。比如说:
var fruits = ["apple", "banana", "orange", "pear"]; // 使用方括号
另一种是使用 new Array()
构造函数,在学习了面向对象编程之后,你就会知道这是一种创建对象的方法。
使用这种方法时,你需要在括号里面写上你想要存储的元素,用逗号分隔。比如说:
var fruits = new Array("apple", "banana", "orange", "pear"); // 使用构造函数
两种方法都可以创建空数组,也就是没有任何元素的数组。比如说:
var empty1 = []; // 使用方括号
var empty2 = new Array(); // 使用构造函数
而如果你想要创建一个只有一个元素的数组,那么使用方括号就很简单,直接写上那个元素就行了。比如说:
var one = ["hello"]; // 使用方括号
但是如果你想要使用构造函数来创建一个只有一个元素的数组,那么你就要注意了。 因为如果你在方括号里面写上一个数字,那么它不会被当作元素,而是被当作数组的长度。比如说:
var wrong = new Array(5); // 这不是一个只有5这个元素的数组
console.log(wrong); // 输出[空属性 × 5],也就是一个长度为5但没有任何元素的空数组
所以如果你想要使用构造函数来创建一个只有一个数字元素的数组,那么你要在方括号里面再加上一对方括号,把数字包起来。比如说:
var right = new Array([5]); // 这才是一个只有5这个元素的数组
console.log(right); // 输出[5]
当然了,如果你想要创建一个只有一个非数字元素的数组,那么使用构造函数就不需要再加上一对方括号了:
var ok = new Array("hello"); // 这也是一个只有"hello"这个元素的数组
console.log(ok); // 输出["hello"]
总之,创建数组的时候建议使用方括号 []
这种方法,因为它更简单、更直观、更不容易出错。而且还能减轻记忆负担!
更改数组的值
我们已经知道了怎么创建数组和怎么访问数组中的元素了,就好比我们知道怎么创建变量和怎么给变量赋值一样。
而数组作为一个容器,我们自然要对其中的数据进行操作。在这一节中,我们先学习使用数据的一些基本高级操作: 比如往数组中添加元素、修改数组中的元素、删除数组中的元素等等。
修改
修改数组中的元素很简单,只需要给指定索引的位置赋值就行了。比如说:
var fruits = ["apple", "banana", "orange", "pear"];
fruits[0] = "grape"; // 把第一个元素改成"grape"
console.log(fruits); // 输出["grape", "banana", "orange", "pear"]
修改数组中的元素并不会改变数组的长度,除非你的修改越界了。
添加
添加元素到数组中有两种方法,一种是使用 push()
方法,在数组的末尾添加一个或多个元素,并返回新的长度。
另一种是使用 unshift()
方法,在数组的开头添加一个或多个元素,并返回新的长度。
方法
数组的方法就像是一些工具,它们可以帮助我们对数组进行各种操作,就像我们用锤子、钳子、剪刀等工具来处理各种物品一样。 数组的方法有很多,我们只介绍一些常用的。你可以像下面这样采用类比的方式来理解它们:
push()
方法就像是一个推车,它可以把一个或多个元素推到数组的末尾,并告诉我们新的长度。unshift()
方法就像是一个挖掘机,它可以把一个或多个元素挖到数组的开头,并告诉我们新的长度。
要使用数组的方法,只需要在数组变量后面加上点号.
,然后再加上方法名,最后加上括号()
就行了。括号里面可以放一些参数,就像是工具的设置一样,可以调整工具的功能和效果。比如说:
var fruits = ["apple", "banana", "orange", "pear"];
fruits.push("watermelon"); // 使用推车把 "watermelon" 推到末尾
console.log(fruits); // 输出 ["apple", "banana", "orange", "pear", "watermelon"]
fruits.unshift("lemon"); // 使用挖掘机把 "lemon" 挖到开头
console.log(fruits); // 输出 ["lemon", "apple", "banana", "orange", "pear", "watermelon"]
删除
删除元素从数组中也有两种方法,一种是使用 pop()
方法,在数组的末尾删除最后一个元素,并返回被删除的元素。另一种是使用 shift()
方法,在数组的开头删除第一个元素,并返回被删除的元素。比如说:
var fruits = ["apple", "banana", "orange", "pear"];
var last = fruits.pop(); // 在末尾删除最后一个元素,并把它赋值给last变量
console.log(last); // 输出"pear"
console.log(fruits); // 输出["apple", "banana", "orange"]
var first = fruits.shift(); // 在开头删除第一个元素,并把它赋值给first变量
console.log(first); // 输出"apple"
console.log(fruits); // 输出["banana", "orange"]
如果你想要删除或替换指定位置或范围内的元素,并返回被删除或替换掉的元素组成的新数组,那么你可以使用splice()
方法。
这个方法需要传入三个参数:第一个参数是开始位置(索引);第二个参数是删除或替换掉多少个元素;第三个参数(可选)是用来替换掉被删除或替换掉的元素。比如说:
var fruits = ["apple", "banana", "orange", "pear"];
var removed1 = fruits.splice(1, 2); // 从索引为1(第二个)开始删除2个元素,并把它们赋值给removed1变量
console.log(removed1); // 输出["banana", "orange"]
console.log(fruits); // 输出["apple", "pear"]
var removed2 = fruits.splice(1, 1, "grape"); // 从索引为1(第二个)开始删除1个元素,并用"grape"替换掉它,并把被替换掉的元素赋值给removed2变量
console.log(removed2); // 输出["pear"]
console.log(fruits); // 输出["apple", "grape"]
注意,在这里我们没有传入第三个参数时,默认为删除操作;传入第三个参数时,默认为替换操作。
纸上得来终觉浅,绝知此事要躬行。对于上面的方法,你可以自己动手试一试,创建一个具有现实意义的需求,然后用数组的方法解决它们。 当然,你也可以尝试在本章末的课后习题巩固这些方法。
遍历数组
有时候,我们的操作涉及到数组中的每个元素,我们自然不能手动地一个一个地去处理它们,就像下面这样:
sum = arr[0] + arr[1] + ... + arr[n]
当 n
比较大的时候,相信不需要提醒你也不会这么干。
更好的方法是遍历数组!所谓遍历数组,就是把数组中的每个元素都访问一遍,对它们进行一些操作或者处理。
比如说,你有一个数组,里面存放了一些学生的成绩,你想要计算他们的平均分,或者找出最高分和最低分,那么你就需要遍历这个数组。
在JavaScript中,有很多种方法可以用来遍历数组。 这些方法有些是从其他语言借鉴过来的,有些是JavaScript特有的。它们各有优缺点,适用于不同的场景。我们来一一介绍它们,并且比较它们之间的异同。
for循环
最基本的遍历方法就是使用我们在上一章介绍的 for
循环,它是用来解决重复代码的好工具!
for
循环是一种通用的循环语法,可以用在大部分编程语言中。我们可以利用数组的长度属性 length
,以及索引的规律,从头到尾依次访问数组中的元素。
属性
属性是对象的状态,比如说,一个人的年龄、身高、体重等等,都是这个人的属性。同样地,数组也有属性,比如说,数组的长度 length
就是它的一个属性。
关于更多属性的内容,我们将在关于对象的章节中详细介绍。
现在你只需要知道,数组的长度属性 length
是一个数字,它表示数组中元素的个数。通过在数组名后面加上 .length
,我们就可以访问到这个属性。
如果觉得有点抽象,可以看下面这个例子:
要遍历数组,我们首先要有一个数组,以及一些存放处理结果的变量:
var scores = [85, 90, 95, 80, 75]; // 存放学生的成绩
var sum = 0; // 存放成绩总和
var max = 0; // 存放最高分
var min = 100; // 存放最低分
基于上一章循环的思想,我们发现每次对数组数据的抽取,无非就是 scores[n]
的形式,其中 n
是索引,从0开始,到 scores.length - 1
结束。
每步操作显然是高度重复的,因此我们可以像下面这样通过 for
循环来解决:
for (var i = 0; i < scores.length; i++) { // i在这作为索引,从0开始,到scores.length-1结束
var score = scores[i]; // 取出当前索引对应的元素值,即成绩
sum += score; // 累加成绩到总和
if (score > max) { // 如果当前成绩大于最高分
max = score; // 更新最高分
}
if (score < min) { // 如果当前成绩小于最低分
min = score; // 更新最低分
}
}
通过 for
循环,我们把数组的每个数据从头到尾看了一遍,因此,max
自然就是正确的最大值,min
自然就是正确的最小值;sum
自然也就是完整的和。
现在我们可以输出结果了:
var average = sum / scores.length; // 计算平均分
console.log("平均分是:" + average); // 输出平均分
console.log("最高分是:" + max); // 输出最高分
console.log("最低分是:" + min); // 输出最低分
使用 for
循环遍历数组的优点是简单直观,可以灵活地控制循环的条件和递增量(比如上面的例子中我们取了每一项数据,
通过控制递增量,我们可以取每一奇数/偶数项),也可以随时中断或跳出循环。缺点是需要手动管理索引变量,容易出错或者写错。
for...of循环
当你不想手动管理索引变量时,还有一种新的方法可以用来遍历数组:
for...of
循环。这是ES6新增的一种循环语法,它可以遍历数组,也可以遍历其他可迭代对象(我们将在关于对象的章节中介绍),并获取它们的值。
它的用法是这样的:
for (variable of iterable) {
statement;
}
其中,variable
是每次迭代时获取到的值(不是索引),iterable
是要遍历的可迭代对象(比如数组),statement
是要执行的语句(可以有多条)。
举个例子,我们可以用 for...of
循环来重写上面的例子:
var scores = [85, 90, 95, 80, 75]; // 存放学生的成绩
var sum = 0; // 存放成绩总和
var max = 0; // 存放最高分
var min = 100; // 存放最低分
for (let score of scores) { // 定义一个变量score表示每次迭代时获取到的值(不是索引)
sum += score; // 累加成绩到总和
if (score > max) { // 如果当前成绩大于最高分
max = score; // 更新最高分
}
if (score < min) { // 如果当前成绩小于最低分
min = score; // 更新最低分
}
}
var average = sum / scores.length; // 计算平均分
console.log("平均分是:" + average); // 输出平均分
console.log("最高分是:" + max); // 输出最高分
console.log("最低分是:" + min); // 输出最低分
可以看到,使用 for...of
循环后,代码也变得更简洁了。我们不需要再关心索引和长度,只需要关注每个元素的值即可。
使用 for...of
循环遍历数组的优点是简单易用,不需要手动管理索引变量,并且支持中断或跳出循环(使用break或continue)。
缺点是不能直接获取索引(如果需要索引,则要使用entries()方法),也不能返回任何值。
forEach 方法
除了上面两个 for
,我们还有 forEach()
方法可以帮助你遍历数组元素。
它的用法是这样的:
array.forEach(callbackFn(currentValue, index, arr));
其中,array
是要处理的数组,callbackFn
是一个回调函数,
它会告诉 forEach()
方法你想对每个元素做什么事情,并传入三个参数:当前元素的值、当前元素的位置、以及数组本身。
那么什么是回调函数呢?我们又为什么要用它呢?我们来用一个简单的比喻来理解一下:
假设你想要去理发店理发,但是你发现理发店很忙,有很多人在等待。你不想浪费时间在那里等着,你想要去做一些其他的事情。你怎么办呢?
你可以把你的电话号码留给Tony老师,并告诉他:“当轮到我时,请给我打电话。”然后你就可以离开理发店去做其他事情了。当Tony打电话给你时,你就知道该回去理发了。
这里,“当轮到我时,请给我打电话”就相当于一个回调函数。它是一个函数(打电话),它被作为参数(电话号码,理解为一个函数名)传入另一个函数(Tony老师)。 当某些操作结束后(Tony老师忙完了,轮到我了),该函数被调用(打电话给我)。
这样做有什么好处呢?你不再需要一直干等Tony老师空闲下来,这期间你可以利用等待的时间去做其他事情。这样就提高了时间利用效率。
同样地,在编程中,我们也可以利用回调函数来实现类似的效果。我们不需要一直等待某个函数执行完毕, 我们可以把我们想要在函数执行完毕后做的事情封装成一个回调函数,并传递给该函数。当该函数执行完毕后,它会自动调用我们传入的回调函数。
举个例子,我们可以用 forEach()
方法来重写上面的例子:
var scores = [85, 90, 95, 80, 75]; // 这是学生的分数
var sum = 0; // 存放成绩总和
var max = 0; // 存放最高分
var min = 100; // 存放最低分
scores.forEach(function(score) { // 这是我们写的回调函数,它会告诉 forEach() 方法我们想对每个分数做什么事情
sum += score; // 把每个分数加到总和里
if (score > max) { // 如果当前分数大于最高分
max = score; // 更新最高分
}
if (score < min) { // 如果当前分数小于最低分
min = score; // 更新最低分
}
});
var average = sum / scores.length; // 计算平均分
console.log("平均分是:" + average); // 输出平均分
console.log("最高分是:" + max); // 输出最高分
console.log("最低分是:" + min); // 输出最低分
上面代码中的回调函数是怎么执行的呢?我们来看一下执行过程:
- 首先,我们写了一个回调函数,并把它作为参数传递给
forEach()
方法。 - 然后,
forEach()
方法开始处理数组scores
中的每个元素。 - 对于每个元素,
forEach()
方法会把它的值、位置和数组本身作为参数传递给回调函数,并执行该函数。 - 回调函数根据传入的参数进行相应的操作,并返回。
forEach()
方法继续处理下一个元素,并重复步骤3和4。- 当所有元素都处理完毕后,
forEach()
方法返回。 - 我们继续执行后面的代码,并输出结果。
可以看到,在这个过程中,回调函数并没有被我们直接调用,而是被 forEach()
方法间接地调用。
这就是为什么它叫做回调函数:它被传递给另一个函数,并在某个时刻被该函数回头再调用。
同样地,使用 forEach()
方法后,代码变得更简洁了。我们不需要再关心位置和长度,只需要关注每个元素的值即可。
当然,如果你需要用到位置或者数组本身,你也可以在回调函数中添加相应的参数(第二个参数和第三个参数)。
使用 forEach()
方法时不能中断或跳出循环(除非出错),也不能返回任何值。
其他方法
除了上面介绍的三种方法外,还有一些其他的方法可以用来遍历数组。这些方法都属于数组自带的方法,并且都接受一个回调函数作为参数。它们各有特点和用途,在某些场景下会比上面的方法更方便或高效。我们来一一介绍它们,并且比较它们之间的异同。
map 方法
这个方法的作用是把数组里的每个元素都变成另一种东西,然后放到一个新的数组里。 它会让你写一个函数(实际上是一个回调函数),这个函数会告诉它怎么把一个元素变成另一个元素。
比如说,你可以用 map()
方法来把学生的分数变成等级:
var scores = [85, 90, 95, 80, 75]; // 这是学生的分数
var grades = scores.map(function(score) { // 这是你写的函数,它会告诉 map 怎么把分数变成等级
if (score >= 90) { // 如果分数大于等于90
return "A"; // 就变成"A"等级
} else if (score >= 80) { // 如果分数大于等于80
return "B"; // 就变成"B"等级
} else if (score >= 70) { // 如果分数大于等于70
return "C"; // 就变成"C"等级
} else if (score >= 60) { // 如果分数大于等于60
return "D"; // 就变成"D"等级
} else { // 否则
return "E"; // 就变成"E"等级
}
});
console.log(grades); // 输出["B", "A", "A", "B", "C"]
用 map()
方法的好处是它不会改变原来的数组,而是给你一个新的数组,并且你可以自由地决定怎么改变每个元素,新数组中每个元素都是回调函数返回的结果。
同时它也有一些不方便的地方,比如说,你不能中途停止或跳过某些元素(除非出错),也不能直接看到元素在数组里的位置(如果需要看到位置,就要用第二个参数)。
filter 方法
这个方法的作用是从数组里挑出一些你想要的元素,然后放到一个新的数组里。 它也会让你写一个函数,这个函数会告诉它哪些元素是你想要的,哪些元素是你不想要的。
比如说,你可以用 filter()
方法来找出学生中及格的分数:
var scores = [85, 90, 95, 80, 75]; // 这是学生的分数
var passed = scores.filter(function(score) { // 这是你写的函数,它会告诉 filter 哪些分数是及格的
return score >= 60; // 只要分数大于等于60,就是及格的
});
console.log(passed); // 输出[85, 90, 95, 80, 75]
用 filter()
方法的好处是它也不会改变原来的数组,而是给你一个新的数组,并且你可以自由地决定哪些元素符合你的条件,因为新数组中只包含回调函数返回真值(true)的元素。
但它同样也有一些不方便的地方,比如说,你也不能中途停止或跳过某些元素(除非出错),也不能直接看到元素在数组里的位置(如果需要看到位置,就要用第二个参数)。
tips
关于数组还有很多其他方法,你可以参阅MDN:Array 部分的内容。 同时,你还可以在评论区分享你对本文提到的方法的使用心得和体会,也可以分享你知道的其他方法。
总结
在这篇博客中,我们学习了以下内容:
数组是一种数据类型,它可以存放多个值,并按照顺序排列。
创建数组的方法有两种:使用方括号或者使用 Array 构造函数。
更改数组的值有三种方式:修改、添加和删除。修改是指改变数组中某个位置的值,添加是指在数组的末尾或者中间插入新的值,删除是指从数组的末尾或者中间移除某个值。
遍历数组是指对数组中的每个元素进行一些操作。我们可以通过 for 循环、for...of 循环、forEach 方法以及 map 方法、filter 方法等方式遍历数组。 for 循环和 for...of 循环都需要自己控制索引和循环条件,而forEach 方法和 map 方法、filter 方法等则不需要,它们会自动处理每个元素, 我们只需要传入一个回调函数来定义我们想做的事情。
在下一章,我们将学习如何使用对象来表示复杂的数据,并探索对象与面向对象编程对于提高代码的可读性和可扩展性的帮助。在此之前,请先尝试完成下面的习题吧!
小试牛刀
- 数组的索引是从几开始的?(单选)
- A. 0
- B. 1
- C. 2
- D. 3
- 下面哪种方法可以创建一个空数组?(多选)
- A. var array = [];
- B. var array = new Array();
- C. var array = Array();
- D. var array = {};
- 下面哪种方法可以在数组的末尾添加一个元素?(单选)
- A. array.push(element);
- B. array.pop(element);
- C. array.unshift(element);
- D. array.shift(element);
- 下面哪种方法可以删除数组的第一个元素?(单选)
- A. array.push(element);
- B. array.pop(element);
- C. array.unshift(element);
- D. array.shift(element);
- 下面哪种方法可以遍历数组,并对每个元素执行一个回调函数?(多选)
- A. for循环
- B. for...of循环
- C. forEach 方法
- D. map 方法
- 写一个函数,接受一个数组作为参数,返回数组中最大的元素。
示例:
function max(array) {
// 在这里写你的代码
}
console.log(max([1, 2, 3])); // 输出3
console.log(max([-1, -2, -3])); // 输出-1
- 写一个函数,接受一个数组作为参数,返回数组中所有元素的平均值。
示例:
function average(array) {
// 在这里写你的代码
}
console.log(average([1, 2, 3])); // 输出2
console.log(average([10, 20, 30])); // 输出20
- 写一个函数,接受一个数组作为参数,返回一个新数组,新数组中每个元素都是原数组中元素的平方。
示例:
function square(array) {
// 在这里写你的代码
}
console.log(square([1, 2, 3])); // 输出[1, 4, 9]
console.log(square([-1, -2, -3])); // 输出[1, 4, 9]
- 写一个函数,接受一个数组作为参数,返回一个新数组,新数组中只包含原数组中大于0的元素。
示例:
function positive(array) {
// 在这里写你的代码
}
console.log(positive([1, -2, 3])); // 输出[1, 3]
console.log(positive([-1, -2, -3])); // 输出[]