前言
在JS中,数组是一个十分重要的知识点,不论是备战面试的八股中,还是各式各样的算法里,掌握数组的都是很必要的,本文我们来深度剖析数组这个知识点,从各个角度去讲解数组的特性与本质,话不多说,就让我们直接开始吧!
数组是对象
在 JavaScript 中,实际上是没有单独的数组的,我们所说的数组本质上是一种特殊的对象。虽然它用起来可以像其它语言使用数组一样,比如通过arr[索引]的方式来使用,但是你需要清楚的是,在JS中,它的底层是实现是基于对象的键值对结构。
正是因为JS中的数组是特殊的对象,所以相对于其它语言来说,JS的数组有着许多独特的特性!这也是我们接下来要探讨的!
1.数组可以定义容量 也可以扩容
在大多数强类型语言(如 Java、C++)中,数组的长度是固定的,一旦声明就无法改变。但在 JavaScript 中,数组是动态的,可以随时扩展或缩减:
let arr = [1, 2, 3]; // 初始长度为 3
arr[5] = 6; // 动态扩容,此时数组长度为 6,中间空位填充 undefined
console.log(arr); // [1, 2, 3, undefined, undefined, 6]
在JS中,数组既可以自定义长度,也可以动态变更长度!
2.数组不限类型
传统语言的数组,必须指定为某一具体类型,但在js中,由于数组就实际上是键值对,所以,无所谓类型,同一个数组中可以存储任意类型的元素
JavaScript 数组的本质:
// 数组在底层类似于这样的对象
const arrayLike = {
"0": "first",
"1": 42,
"2": { name: "object" },
"3": [1, 2, 3],
length: 4
};
混合类型数组示例:
const mixedArray = [
42, // Number
"Hello", // String
{ name: "Object" }, // Object
[1, 2, 3], // Array
true, // Boolean
null, // Null
undefined, // Undefined
function() { // Function
return "I'm a function";
},
new Date(), // Date
/regex/, // RegExp
Symbol("foo") // Symbol
];
console.log(mixedArray.length); // 11
你看吧,这个数组mixedArray存储了几乎所有的数据类型!
数组可以当作各种数据结构--链表、队列、栈
由于 JavaScript 数组的动态性和灵活性,它可以模拟多种数据结构:
栈(Stack) :使用 push() 和 pop() 实现后进先出(LIFO)。
// 使用数组模拟栈
const stack = [];
// 入栈 - 使用push()方法
stack.push('A');
stack.push('B');
stack.push('C');
console.log(stack); // ['A', 'B', 'C']
// 出栈 - 使用pop()方法
const lastItem = stack.pop();
console.log(lastItem); // 'C'
console.log(stack); // ['A', 'B']
// 查看栈顶元素(不弹出)
const topItem = stack[stack.length - 1];
console.log(topItem); // 'B'
队列(Queue) :使用 push() 和 shift() 实现先进先出(FIFO)。
// 使用数组模拟队列
const queue = [];
// 入队 - 使用push()方法
queue.push('A');
queue.push('B');
queue.push('C');
console.log(queue); // ['A', 'B', 'C']
// 出队 - 使用shift()方法
const firstItem = queue.shift();
console.log(firstItem); // 'A'
console.log(queue); // ['B', 'C']
// 查看队首元素(不出队)
const frontItem = queue[0];
console.log(frontItem); // 'B'
链表(Linked List) :虽然 JavaScript 没有内置链表,但可以通过对象和数组模拟其行为。
// 定义链表节点
class ListNode {
constructor(value) {
this.value = value;
this.next = null;
}
}
// 创建链表
const node1 = new ListNode('A');
const node2 = new ListNode('B');
const node3 = new ListNode('C');
// 连接节点
node1.next = node2;
node2.next = node3;
// 遍历链表
function traverseList(head) {
let current = head;
while (current !== null) {
console.log(current.value);
current = current.next;
}
}
traverseList(node1); // 输出: A B C
// 在链表末尾添加节点
function appendNode(head, value) {
const newNode = new ListNode(value);
if (!head) return newNode;
let current = head;
while (current.next !== null) {
current = current.next;
}
current.next = newNode;
return head;
}
// 使用数组初始化链表
function createListFromArray(arr) {
if (!arr.length) return null;
const head = new ListNode(arr[0]);
let current = head;
for (let i = 1; i < arr.length; i++) {
current.next = new ListNode(arr[i]);
current = current.next;
}
return head;
}
const myList = createListFromArray(['X', 'Y', 'Z']);
traverseList(myList); // 输出: X Y Z
JS就是那么神奇!它没有自己独立的栈、队列等数据结构,妙用数组,可以既当栈又当队列!
数组的声明方法
JavaScript 提供了多种声明数组的方式:
-
字面量方式(最常用):
let arr1 = [1, 2, 3]; -
构造函数方式:
let arr2 = new Array(1, 2, 3); // [1, 2, 3] -
ES6 的
Array.of()(解决构造函数歧义):let arr4 = Array.of(3); // [3],而不是长度为 3 的空数组这里提一嘴:需要注意
new Array()和Array.of()的区分
new Array(3)是声明一个长度为3的数组[undefied,undefied,undefied]Array.of(3)是声明一个长度为1的数组[3]
-
ES6 的
Array.from()(将类数组或可迭代对象转为数组):let arr5 = Array.from("hello"); // ['h', 'e', 'l', 'l', 'o'] // 传入第二个参数:回调函数 console.log(Array.from(new Array(26),(val,index) => String.fromCodePoint(65 + index)))注意Array.from可以接受第二个参数
mapFn用来调用数组每个元素的函数,如果提供,每个将要添加到数组中的值首先会传递给该函数,然后将mapFn的返回值增加到数组中
数组的遍历方式
JavaScript 提供了多种遍历数组的方法,每种方法都有其适用场景和特点。下面我将详细补充各种遍历方式的特性、优缺点和使用场景。
1. 传统 for 循环 (for(let i...))
const arr = [10, 20, 30];
for (let i = 0; i < arr.length; i++) {
console.log(`索引 ${i} 的值是 ${arr[i]}`);
}
特点:
最基础的循环方式
通过索引访问数组元素
性能最优(对CPU最友好)
2. for...in 循环
const arr = [10, 20, 30];
Array.prototype.customProp = '不要遍历我'; // 在原型上添加属性
for (let index in arr) {
console.log(`索引 ${index} 的值是 ${arr[index]}`);
// 会输出: 0 10, 1 20, 2 30, 还会输出 customProp '不要遍历我'
}
//也可以这样用
const arr = [1,2,3];
for(const [index,value] of arr.entries()) {
// entries 返回可迭代的对象,每一项都是数组,第一项是key,第二项是值
console.log(index,value);
}
特点:
遍历对象的可枚举属性(包括原型链上的属性)
这点很重要,只有for...in这一种遍历方式会遍历原型链上的数据
可以配合数组的迭代器arr.entries()来使用
性能比传统for循环差
3. for...of 循环 (ES6)
const arr = [10, 20, 30];
for (let value of arr) {
console.log(value); // 10, 20, 30
}
特点:
直接遍历数组的值而非索引
语法简洁明了
性能略低于传统for循环
4. forEach 方法
const arr = [10, 20, 30];
arr.forEach((value, index, array) => {
console.log(`索引 ${index} 的值是 ${value}`);
// 第三个参数 array 是原始数组的引用
});
const names = Array.of('Alice','Bob','Charlie','David');
names.forEach(name => {
if(name === 'Charlie') {
console.log('Charlie is here,stop...')
return;
}
console.log('Processing' + name);
})
//输出结果为:
......
Charlie is here,stop...
ProcessingDavid
特点:
高阶函数,接收回调函数
提供值、索引和原数组三个参数
不能使用 break 或 continue,无法中途跳出循环(除非抛出异常)
这个forEach比较特别,我们需要注意它的特点:forEach 没有办法像 for 或 while 那样用 break 或 return 来中断整个循环。这是因为 forEach 的回调函数里的 return 只会结束当前这一次的回调(即跳出当前这一次的箭头函数),不会终止整个 forEach 循环。
如果上面的代码是这样写的:
if(name === 'Charlie') {
console.log('Charlie is here,stop...')
return;
console.log("hello")
}
它的回调函数里的 return 只会结束当前这一次的回调(不会执行console.log("hello")),但是forEach 还是会继续对下一个元素('David')执行回调。
5. 其他遍历方法补充
map (返回新数组)
const doubled = arr.map(value => value * 2);
filter (返回过滤后的新数组)
const evens = arr.filter(value => value % 2 === 0);
reduce (累积计算)
const sum = arr.reduce((acc, value) => acc + value, 0);
拔高部分--稀疏数组
接下来介绍关于数组的高阶知识,在面试中,关于数组的知识,可能只有2%的高手才能达到这个高度哟
稀疏数组
观察下面的代码,请回答会输出什么?
const arr = new Array(5); // 指定了大小,并未使用fill指定元素
console.log(arr)
答: 在浏览器的环境中,会输出以下:
在node的环境中,会输出以下:
原因解释:
- new Array(5) 创建了一个长度为 5的数组,但没有实际赋值,每个位置都是“empty slot”(空槽),不是 undefined,而是“没有值”。
- 这种数组叫做“稀疏数组”(sparse array),它有长度,但每个元素都还没被初始化。
- 所以 console.log(arr) 显示的是 5 个 empty(空槽),而不是 [undefined, undefined, undefined, undefined, undefined]。
稀疏数组有什么意义?
稀疏数组(Sparse Array)指的是数组长度大于实际存储的元素个数,中间有“空槽”(empty slot),即有些索引没有值。比如:
const arr = new Array(5); // [ <5 empty items> ]
arr[2] = 'hello'; // [ <2 empty items>, 'hello', <2 empty items> ]
它的意义:
- 节省内存(理论上)
如果你只需要在很大的索引范围内存储少量数据,稀疏数组可以避免为每个索引都分配空间(但 JS 引擎实现未必真的节省很多内存)。
- 模拟“稀疏矩阵”或“稀疏表”
在某些算法或数据结构中,数据分布很稀疏(比如棋盘、地图、矩阵等),用稀疏数组可以只存有用的数据,未赋值的地方就是“空”。
- 兼容性和特殊需求
有些第三方库、API、老代码会用稀疏数组来表示“未定义”或“未赋值”的状态。
- 性能测试/边界测试
有时为了测试数组方法在稀疏数组上的表现(比如 forEach、map、filter 等),会特意构造稀疏数组。
注意:
- 稀疏数组的“空槽”不是 undefined,而是“没有值”。
- 很多数组方法会跳过空槽(如 forEach、map、filter),但 for...in、Object.keys 会遍历有值的索引。
- 稀疏数组的意义主要在于节省空间、模拟稀疏数据结构、兼容特殊需求。但在实际开发中,除非有特殊需求,否则更推荐用对象或 Map 来存储稀疏数据。
总结
JavaScript 数组是一种灵活且强大的数据结构,它既是对象,又能模拟多种数据结构。掌握其声明方式、遍历方法以及底层特性,能够帮助我们在实际开发中更高效地使用它。希望本文对你理解 JavaScript 数组有所帮助! 🚀