八、数组
8.1 概念
数组也是一种复合数据类型,在数组可以存储多个不同类型的数据,任何类型的值都可以成为数组中的元素。
数组中存储的是有序的数据,数组中存储的数据叫做元素,数组中的元素都有一个唯一的索引,可以通过索引来操作获取数据,索引(index)是一组大于0的整数。
8.1.1 创建数组
通过Array()
来创建数组
let arr = Array()
console.log(typeof arr);
console.log(arr);
可见,数组实际上是一个对象类型的数据,该对象包含一些定义在其内部的方法支持完成诸如插入修改删除等对数据的操作
为了方便,也可以通过[]
来创建数组,这相当于数组的字面量
let arr = []
console.log(typeof arr);
console.log(arr);
8.1.2 向数组中添加元素
语法:数组[索引]
let arr = []
arr[0] = 123
arr[1] = 456
console.log(arr[0]);
console.log(arr[1]);
console.log(arr);
如果你执行下面的代码:
let arr = []
arr[0] = 123
arr[100] = 456
console.log(arr);
一个数组,你在索引0和100的位置上定义了数据,在别的编程语言中说不定就报错, 但是在JavaScript他不仅不会报错,还会…
贴心的帮你在0和100之间添加省略号,当我们输出他的length
(length表示数组的长度)
console.log(arr.length);
他会打印101,即该数组里面一共有101个元素。
and,真的感觉有点恶心了,来看下面这条代码:
let arr = []
arr[2.5] = 123
console.log(arr);
console.log(arr.length);
debugger // 开启调试
我们可以看到,arr[2.5]其实是有数据的,而且使用arr[2.5]
是能够正确打印的,但数组的长度仍然是0.
同时arr["你好"]
也能成功定义并输出,你可以自己试试
8.1.2 读取数组中的元素
语法:数组[索引]
上面写添加的代码里面已经包含读取了,不再赘述
8.1.3 length
获取数组的长度
arr.length
获取的实际值就是数组的最大索引 + 1
向数组末尾追加元素:
数组[数组.length] = 元素
另外,length是可以修改的…你敢信?
// 实践:修改数组的length
let arr = []
for (let index = 0; index < 10; index++) {
arr[index] = index
}
console.log(arr.length);
arr.length = 50
console.log(arr.length);
console.log(arr);
我们在进行如下实验
let arr = []
for (let index = 0; index < 10; index++) {
arr[index] = index
}
console.log(arr.length);
arr.length = 50 // 修改数组的length属性
console.log(arr.length); // 查看length属性是否被修改
console.log(arr); // 打印数组
arr[10] = 10 // 数组添加元素
arr[11] = 11 // 数组添加元素
console.log(arr.length); // 追加数组元素后查看length是否会加一
for (let index = 10; index < 55; index++) {
arr[index] = index // 添加多个数据,使数组内元素超过我们定义的50,看length是否会发生变化
}
console.log(arr.length); // 追加数组元素后查看length是否会加一
debugger
实验结果:
- 数组的length属性可以自由修改
- 修改length后再次添加元素
- 如果总的元素数量小于修改后的length值,则保留修改后的length值
- 否则使用正确的length
8.2 数组遍历
遍历数组简单理解,就是轮流获取到数组中的每一个元素
let arr = [1,2,3,4,5,6,7]
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// 1 2 3 4 5 6 7
这就是遍历,通过修改for循环内部的初始化和判断条件,可以自由实现正序或者倒序打印
根据数组中保存的数据类型的不同,可以在for循环体内部编写合适的语句实现正确的遍历。
实际上还是for循环的使用
8.2.1 for-of语句
let arr = [1,2,3,4,5,6,7]
arr[2.5] = "你好"
for (const e of arr) {
console.log(e);
}
for-of的循环体会执行多次,数组中有几个元素就会执行几次,每次执行时都会将一个元素赋值给变量。
8.3 数组的方法
之前我们已经说过,数组实际上是一个对象,该对象内部定义了很多方法供我们调用,可以点击查看有哪些方法
我们现在稍微介绍一些实用性比较强且使用频繁的方法
8.3.1 Array.isArray()
- 用来检查一个对象是否是数组
因为使用typeof
检查时返回的是Object
所以我们需要一个方法来判断Array是不是Array
let arr = [1,2,3,4,5,6,7]
console.log(Array.isArray(arr)); // true
8.3.2 Array.at()
-
可以根据索引获取数组中的指定元素
-
at可以接收负索引作为参数
let arr = [1,2,3,4,5,6,7]
console.log(arr.at(1)); // 2
console.log(arr.at(-1)) // 7
console.log(arr[-1]); // undefined
8.3.3 Array.concat()
- 用来连接两个或多个数组
- 非破坏性方法,不会影响原数组,而是返回一个新的数组
let arr1 = [1,2,3]
let arr2 = [4,5,6,7]
let arr3 = arr1.concat(arr2)
console.log(arr3); // [1, 2, 3, 4, 5, 6, 7]
也有破坏性的原地操作方法使原数组的数据发生改变
容易想到的是使用for循环,遍历arr2中的数据,并逐个加入到arr1当中。
let arr1 = [1,2,3,4]
let arr2 = [4,5,6,7]
for (let i = 0; i < arr2.length; i++) {
arr1.push(arr2[i])
}
console.log(arr1); // [1, 2, 3, 4, 4, 5, 6, 7]
因为push可以实现多个数据同时插入,例如:
let arr1 = [1,2,3,4]
let arr2 = [4,5,6,7]
arr1.push(888,9,99)
console.log(arr1); // [1, 2, 3, 4, 888, 9, 99]
我们可以想到使用如下代码实现非破坏性方法插入:arr1.push(arr2)
,由于数组可以保存任意类型的数据,所以arr2将会作为一个数组类型的数据而整体保存入arr1中。
let arr1 = [1,2,3,4]
let arr2 = [4,5,6,7]
arr1.push(arr2)
console.log(arr1); // [1, 2, 3, 4, Array(4)]
那么有没有什么办法能够实现如下伪代码呢?
arr1.push(arr2[0],arr2[1],arr3[2] ... ...)
ES6标准推出了展开语法可以实现,展开语法的核心是一个运算符,...
,该运算符称为展开运算符,它可以实现将数组或对象进行展开,示例如下:
let arr1 = [1,2,3,4]
let arr2 = [4,5,6,7]
let person = {
name:"Joe",
age:19,
gender:"男"
}
console.log("数组展开:",...arr1);
console.log("对象展开:",{...person}); // 注意在这里直接打印...person会报错,因为展开后还是key-value形式
/*
数组展开: 1 2 3 4
对象展开: {name: 'Joe', age: 19, gender: '男'}
*/
所以一种比较优秀的数组原地追加的操作如下:
let arr1 = [1,2,3,4]
let arr2 = [4,5,6,7]
arr1.push(...arr2)
console.log(arr1); // [1, 2, 3, 4, 4, 5, 6, 7]
在展开语法之前一般使用Function.prototype.apply
的方式进行调用。apply是function原型中的方法,所以可以使用push方法来调用。
let arr1 = [1,2,3]
let arr2 = [4,5,6,7]
arr1.push.apply(arr1,arr2)
console.log(arr1); // [1, 2, 3, 4, 5, 6, 7]
8.3.4 indexOf()
indexOf()
方法返回数组中第一次出现给定元素的下标,如果不存在则返回 -1。
let arr1 = [1,2,3,4,5,6,7]
console.log(arr1.indexOf(1)); // 0
console.log(arr1.indexOf(100)); // -1
该方法还能接受第二个参数fromIndex,这是一个可选参数,表示开始搜索的索引,必然从前往后检索。
是的indexOf()可以设置第二个参数,但我有时候想不起来用,就会浪费很多性能。
-
如果是正整数,则会按照其值安排检索
- 如果正整数超过了数组的长度,则会直接返回-1
- 否则正常进行检索
-
如果是负整数,则会进行一次计算:
frommindex + array.length
- 如果仍然小于0,将使用0
- 如果成为正整数,则按照正整数的法则
-
如果是是浮点数,则会转换为整数(直接去掉小数点)
8.3.5 lastIndexOf()
lastIndexOf()
方法返回数组中给定元素最后一次出现的索引,如果不存在则返回 -1。该方法从 fromIndex
开始向前搜索数组。
根据indexof() 方法就能猜到该方法是如何进行的了,注意该方法必然从后往前检索 就可以了。
8.3.6 join()
join()
方法将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串,用逗号或指定的分隔符字符串分隔。如果数组只有一个元素,那么将返回该元素而不使用分隔符。
let arr1 = [1,2,3,4,5,6,7]
console.log(arr1.join("|")); // 输出:1|2|3|4|5|6|7
let obj1 = { // 类数组对象
length:4,
0:0,
1:1,
2:2,
3:3
}
console.log(Array.prototype.join.call(obj1,"|")); // 输出:0|1|2|3 ; 入门阶段不做要求
8.3.7 slice()
slice()
方法返回一个新的数组对象,这一对象是一个由 start
和 end
决定的原数组的浅拷贝(包括 start
,不包括 end
),其中 start
和 end
代表了数组元素的索引。原始数组不会被改变。
值得注意的是,start和end都是可选参数,start默认是0,end默认是arr.length,所以arr.slice()
及arr.slice(0)
都可以实现浅拷贝整个数组
let arr = [1,2,3,4,5,6,7,8,9]
console.log(arr.slice(1,2)); // [2] 左闭右开 [1,2) 包含1而不包含2
console.log(arr.slice(1,200)); // [2, 3, 4, 5, 6, 7, 8, 9] 超出数组长度的部分忽略
console.log(arr.slice(1,-1)); // [2, 3, 4, 5, 6, 7, 8] 支持负数 负数 = length+负数
console.log(arr.slice()); // 拷贝整个数组
console.log(arr); // [1, 2, 3, 4, 5, 6, 7, 8, 9] 原数组没有发生改变
8.3.7.1 深浅拷贝
let arr1 = [1,2,3,{name:"Joe",age:15}]
let arr2 = arr1.slice()
console.log(arr2); // [1, 2, 3, {…}]
console.log(arr1); // [1, 2, 3, {…}]
console.log(arr1 === arr2); // false
arr1[3].age = 150
console.log(arr1[3].age); // 150
console.log(arr2[3].age); // 150
上述代码定义了两个数组arr1和arr2,我们初始化arr1的数据然后使用slice
将arr1的全部数据都拷贝到arr2中。我们期待的是产生如下情形:
但当我们修改arr1数组下标为3的Object类型的数据{name:"Joe",age:15}
中的age属性后,分别打印arr1和arr2的age属性,发现对arr1的修改的同时arr2的数据也随之修改,所以上面图片中的情形是错误的,arr1和arr2之前还有一定的联系。
我们在来看一下Number类型的数据:
arr1[0] = 1000
console.log(arr1[0]); // 1000
console.log(arr2[0]); // 1
发现修改arr1的数据并不会对arr2造成任何影响。
我们推断,Object类型的数据具有特殊性,arr1和arr1拷贝生成的arr2中的Object可能指向的是同一个Object。
我们使用严格相等运算符来检验我们的猜测:
console.log(arr1[3] === arr2[3]); // true
果然是同一个Object
这就是我们所说的浅拷贝
那么深拷贝呢?果然你已经才出来了
这就是深拷贝,原数组与拷贝后的数组的Object不是同一个Object,同时该问题在各种编程语言中普遍出现
那么JavaScript如何实现深拷贝呢?
JavaScript提供了一个专门用来实现深拷贝的方法structuredClone()
因为笔者使用的nodejs版本为16,所以在执行以下代码时,报了个小错:
let arr1 = [1,2,3,{name:"Joe",age:15}]
let arr2 = structuredClone(arr1)
structuredClone is not defined
根据[Solved] structuredClone is not defined in JS and Node.js | bobbyhadz所说
As previously noted, the
structuredClone
method was introduced in Node.js version 17.0.0.If the output of the
node -v
command is less than17
, then you have to install the long-term supported version of Node.js.One way to install the LTS Node.js version is to use the official Node.js website.
structuredClone方法是在Node.js17引入的,所以最好的解决办法是升级到NodeJs17,node安装太麻烦了不搞了,直接在浏览器环境下运行行了。
8.3.8 splice
splice()
方法通过移除或者替换已存在的元素和/或添加新元素==就地==改变一个数组的内容。
该方法名和上述讲过的slice
方法的方法名差不多,注意不要混淆,我刚才其实就混淆了… …
该方法的效果就是从数组截取一部分,要么扔掉,要么替换
所以该方法总共可接收2+个参数,start、deleteCount、+ item1,item2,item3… …
其中start是必选参数,不传deleteCount参数的情况下默认包括后面所有的参数。
item1,item2,item3… … 属于要替换或插入的数据
他们的顺序是start、deleteCount、+ itemN
可将deleteCount赋予值Infinity(最最最最最大),则替换后面所有的参数
返回值是一个包含了删除元素的数组,注意返回的是删除了的元素
PS:可以自己动手实现一下稀疏数组的splice,稀疏数组就是不是装满元素的数组,例如:[1,2,“”,3,“”,4,“”,5,“”,“”,“”]
8.4 练习1:数组去重
题目:[1, 2, 1, 3, 2, 2, 4, 5, 5, 6, 7]是一个数组,然后请你对他进行去重,去重后返回的结果数组要包含原数组中的所有值,但有且仅有一个。
// 数组去重
let arr = [1, 2, 1, 3, 2, 2, 4, 5, 5, 6, 7]
let res = [] // 辅助数组
// for (let i = 0; i < arr.length; i++) {
// if (res.indexOf(arr[i]) == -1) {
// res.push(arr[i])
// }
// }
// 有没有不使用辅助数组的呢
// for (let i = 0; i < arr.length; i++) {
// if (arr.indexOf(arr[i],i+1)!= -1) {
// arr.splice(i,1)
// i -= 1
// }
// }
for (let i = 0; i < arr.length; i++) {
let index = arr.indexOf(arr[i],i+1)
if (index != -1) {
arr.splice(index,1)
i--
}
}
console.log(arr);
8.4 练习2:数组排序
这是一个数组:[1, 2, 1, 3, 2, 2, 4, 5, 5, 6, 7]
请对他进行排序。
排序有什么选择、插入、对比,
自己搜索一下最简单的冒泡排序算法就OK