# 数组
# 数组的基本概念
数组是由相同类型的元素的集合所组成的数据结构,分配一块连续的内存来存储。利用元素的索引(index)可以计算出该元素对应的存储地址,而在 JavaScript 中,数组不是以一段连续的区域存储在内存中的,而是一种哈希映射,所以说 Javascript 中的数组,并不是真正意义上的数组。
# 数组的操作方式
# 初始化
我理解的,数组的创建和初始化差不多是一个意思,为啥是差不多而不是一定,因为玄学上说的话不能说太满,然后应该是有一部分同学理解是比如创建了一个空数组,数组每一项初始化为 0,那这个时候创建和初始化确实可以分开了。我是站在一个游戏现象模型上这样定义的“从无到有”,就是你打开一个游戏,有一条进度条显示初始化进度,是一个“从无到有”的过程,我将其称之为“初始化”。
捋一捋大概就 2 种, 一种是通过字面量的形式搞,一种是通过构造函数的形式搞
楼下两种情况是等价的
var arr = []; // 字面量的形式
var arr2 = new Array(); // 构造函数不传参数的情况
var arr3 = [1, 2, 3, 4, 5]; // 字面量的形式
var arr4 = new Array(1, 2, 3, 4, 5); // 构造函数不传参数的情况
那我们在构造函数传入一个参数(length),默认造出来的数组是啥样的呢 ,它是这样的
const arr = new Array(5);
console.log(arr); // [ <5 empty items> ]
console.log(arr[0]); // undefined
这个肯定不是我们期望的,比如有些场景是需要初始化具体值的,这就需要在原基础上升级了,这样造
const arr = new Array(5).fill(0);
console.log(arr); // [ 0, 0, 0, 0, 0 ]
console.log(arr[0]); // 0
# 读取
这里你也可以叫访问吧,和读取是一个意思。字面上就是该数组加上中括号和对应的下标(数组的下标是从 0 开始的),就可以读取对于的数组元素了。
# 插入
分析了下大概有三种插法,前插,后插,随便插。 都有相关的 API,读者稍微理解一下就好,不用死记硬背。
# 前插 - unshift
var arr = new Array(5).fill(0);
console.log(arr); // [0, 0, 0, 0, 0]
arr.unshift(1, 2, 3);
console.log(arr); // [1, 2, 3, 0, 0, 0, 0, 0], 这里为了演示效果我就多插了几下
# 后插 - push
var arr = new Array(5).fill(0);
console.log(arr); // [0, 0, 0, 0, 0]
arr.unshift(1, 2, 3);
console.log(arr); // [0, 0, 0, 0, 0, 1, 2, 3], 这里为了演示效果我就多插了几下
# 随便插 - splice
const arr = new Array(5).fill(0);
console.log(arr); // [0, 0, 0, 0, 0]
arr.splice(2, 0, 1, 2, 3);
console.log(arr); // [0, 0, 1, 2, 3, 0, 0, 0], 在下标为2的索引开始,插入三个元素1, 2, 3
# 删除
这里也还是有 4 种,一种是 delete, 一种是 splice(是的,它除了可以任意位置插入,还可以任意位置删除), 一种是 pop 从数组末尾删除, 一种是 shift 从数组开头删除
我们先来看下 splice 选手的,可以看到手符合我们的期望的
const arr = new [1, 2, 3, 4, 5]();
console.log(arr); // [1, 2, 3, 4, 5]
arr.splice(2, 1);
console.log(arr); // [1, 2, 4, 5]
我们再来看下 delete 选手的,可以看到是不符合我们的期望的,虽然是被“删除”了,但是长度还是原来的,delete 是删除了表达式、引用类型的结果
const arr =[1, 2, 3, 4, 5];
console.log(arr); // [1, 2, 3, 4, 5]
console.log(delete arr[2]); // true
console.log(arr); // [ 1, 2, <1 empty item>, 4, 5 ]
我们再来看下 pop 选手的
const arr = [1, 2, 3, 4, 5];
console.log(arr); // [1, 2, 3, 4, 5]
arr.pop();
console.log(arr); // [ 1, 2, 3, 4 ]
我们再来看看 shift 选手的
const arr = [1, 2, 3, 4, 5];
console.log(arr); // [1, 2, 3, 4, 5]
arr.shift();
console.log(arr); // [2, 3, 4, 5]
# 修改
同楼上的读取,就是找到对应下标赋上新值, 这里没有相关的静态类型约束,随便改啦。
# 查找
这个比如我要找倒数组中元素为 2 的下标,那么我只需要遍历判断下对应下标的元素的值是不是等于 2 就好了。
const arr = [1, 2, 3, 4, 5, 2];
for (let i = 0; i < arr.length; i++) {
if (arr[i] === 2) {
console.log(i);
}
}
// 1 5
# 遍历
总结了下大概有 5 种, for、 for in、 for of、 forEach 和 map。
for 循环是最常见的遍历方式,大部分语言都是通用的,保险点就用它
const arr = [1, 2, 3, 4, 5, 2];
for (let i = 0; i < arr.length; i++) {
console.log(i, arr[i]);
}
for in 是遍历对应的 key 的
const arr = [1, 2, 3, 4, 5, 2];
for (let key in arr) {
console.log(key, arr[key]);
}
for of 是遍历对应的 value 的
const arr = [1, 2, 3, 4, 5, 2];
for (let value of arr) {
console.log(value);
}
forEach 大部分语言也有,比较普遍,尽量不要去用,因为退不出循环,当一大片代码里面有好多个这种的,除了 bug 不易于排查。
const arr = [1, 2, 3, 4, 5, 2];
arr.forEach((value, index) => {
if (index === 2) {
return;
}
console.log(index, value);
});
# 迭代和迭代器函数
迭代,比较经典的就是斐波那契数列了, 通过前面的值去推导后面的值
const fibonacci = [1, 1, 2];
function fib(n) {
if (n <= 2) {
return fibonacci[n];
}
for (let i = 3; i <= n; i++) {
fibonacci[i] = fibonacci[i - 1] + fibonacci[i - 2];
}
return fibonacci[n];
}
for (let i = 0; i < 10; i++) {
console.log(fib(i));
}
ES6 为数组增加了一个@@iterator 属性,可通过 Symbol.iterator 来访问, 这里故意取到等号,当执行第 11 次iterator.next().value
的时候,值为 undefined,因为迭代完了。
const arr = [7, 2, 4, 5, 9, 6, 3, 8, 10, 1];
let iterator = arr[Symbol.iterator]();
for (let i = 0; i <= arr.length; i++) {
console.log(iterator.next().value);
}
这里还可以通过 for...of 循环迭代
const arr = [7, 2, 4, 5, 9, 6, 3, 8, 10, 1];
let iterator = arr[Symbol.iterator]();
for (const value of iterator) {
console.log(value);
}
entries 方法返回包含键值对的@@iterator
const arr = [7, 2, 4, 5, 9, 6, 3, 8, 10, 1];
let iterator = arr.entries();
for (let i = 0; i < arr.length; i++) {
console.log(iterator.next().value);
}
这里还可以通过 for...of 循环迭代
const arr = [7, 2, 4, 5, 9, 6, 3, 8, 10, 1];
let iterator = arr.entries();
for (const item of iterator) {
console.log(item);
}
此外还有, keys 方法返回包含数组索引的@@iterator, values 方法返回包含数组的值的@@iterator 。这里就不作展开了,读者有兴趣可以了解学习下。
# 置空
一句话哈 arr.length = 0 // now arr is []
# 交换位置
一句话哈 [arr[i], arr[j]] = [arr[j], arr[i]]
# 合并数组
一句话哈 arr.concat(arr1)
, concat 方法返回合并后的数组,但其不改变数组本身。
const arr = [1, 2, 3];
const arr1 = [4, 5, 6];
console.log(arr.concat(arr1)); // [1, 2, 3, 4, 5, 6]
console.log(arr); // [1, 2, 3]
console.log(arr1); // [4, 5, 6]
# 打乱数组
一句话哈, arr.sort((a, b) => Math.random() - 0.5)
我们来看个例子
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
arr.sort((a, b) => Math.random() - 0.5);
console.log(arr.toString()); // 7,2,4,5,9,6,3,8,10,1, 每次都不一样
# 排序
一句话哈, arr.sort((a, b) => a - b)
我们来看下这个例子
const arr = [7, 2, 4, 5, 9, 6, 3, 8, 10, 1];
arr.sort((a, b) => a - b);
console.log(arr.toString()); // 1,2,3,4,5,6,7,8,9,10
# 反转
一句话哈, arr.reverse()
# 转字符串
一句话哈, arr.join('')
或者 arr.toString()
, 后者每个元素中间有逗号连接
# 复制数组
因为 JavaScript 中的引用类型共用内存地址,所以这边就涉及到一个浅拷贝和深拷贝的知识了。
# 浅拷贝
一句话哈, let newArr = arr.slice(0)
# 深拷贝
一句话哈, let newArr = JSON.parse(JSON.stringify(arr))
因为这里是讲数构和算法,所以不过多介绍 JS 数组相关操作的 API,高阶函数这一块了。
# 二维数组和多维数组
二维数组跟矩阵是一个意思,矩阵在生活中很常见,比如下围棋的棋盘, 比如开运动会走过主席台的每一个方块。
二维数组的初始化是这样子的
const arr = new Array(5); // 注意这里不要写成 const arr = new Array(5).fill([]) 因为js中的数组是引用类型
for (let i = 0; i < arr.length; i++) {
arr[i] = [];
}
arr[0][0] = 1;
console.log(arr); // [ [ 1 ], [], [], [], [] ]
多维数组这里以三维数组举例, 其实也很常见,比如要记录正方体的点的坐标,那么就要用到三维坐标系,把 x 轴、y 轴、z 轴的值记录到数组中,就是三维数组啦。
# typescript 中定义数组
用 const 或者 let 声明相关的数组变量, const variableName: <type>[]
, 例如 const arr:number[] = []
# FAQ
# 为什么数组的下标是从 0 开始的而不是 1?
历史问题,JavaScript 除了名字和 JAVA 很像外,好多的思想和设计也是效仿 JAVA 中的设计。最开始是 C 语言中设计数组下标用 0 计数的,后面的高级语言向 JAVA、JavaScript 效仿它的。在 C 语言中数组的内存地址是一块连续的内存地址。
假如数组 a 下标从 0 开始,那么内存地址的公式是
a[k]_address = base_address + k * type_size
而如果下标从 1 开始的话,那么内存地址的公式是
a[k]_address = base_address + (k-1)*type_size
可以看到随机访问在下标开始为 1 的情况下,多做了一次减法运算,对于计算机,CPU 就多做了一次减法指令,这算是人机妥协的典型例子吧。
# 参考文献
- 维基百科 - 数组: https://zh.wikipedia.org/wiki/%E6%95%B0%E7%BB%84
- MDN - 数组: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array
- 极客时间 - 数据结构和算法之美数组为什么下标是 0 开始的: https://time.geekbang.org/column/article/40961