当被问及 "数据结构有哪些" 时,若只罗列 "数组、链表、栈、队列、树",难免显得单薄且缺乏逻辑。其实,数据结构可清晰划分为两大核心类别 ------线性结构与非线性结构,各类结构的设计逻辑和应用场景各有侧重,理解其本质能让我们更灵活地运用。
线性结构是数据按 "一对一" 顺序排列的结构,日常开发中最为常用,核心成员包括数组、链表、栈和队列。其中,数组堪称 "万能基础款",凭借连续的内存空间实现高效随机访问,操作简单、性能稳定,是各类场景的首选;而链表虽不及数组普及,却以离散的内存分配打破了空间连续性限制,能灵活应对动态增删场景,完美弥补了数组的短板。再看栈与队列,二者虽均基于线性逻辑,但遵循截然不同的操作规则:栈是 "先进后出(FILO)" 的 "存取容器",常见于函数调用、表达式求值等场景;队列则是 "先进先出(FIFO)" 的 "排队模型",广泛应用于任务调度、消息队列等需求中。
非线性结构则是数据呈 "一对多" 或 "多对多" 关联的结构,其中树结构最为核心,尤其以二叉树为考察重点 ------ 无论是算法面试还是实际开发中的搜索、排序场景,二叉树及其衍生结构(如二叉搜索树、红黑树)都占据重要地位。
接下来,我们将聚焦线性结构中的 "核心成员"------ 数组,详细拆解它的底层原理、使用场景与实战技巧。
新建数组
html
<script>
const arr = (new Array(6)).fill(0);
console.log(arr);
const arr2 = new Array(6);
console.log(arr2);
//浏览器显示 [空 ×6]
</script>

用 new Array(length) 创建的数组,在没有初始化之前,它的元素是 "空" 的(empty),而不是 undefined 或其他值。
遍历数组 for(i=0;i<len;i++)
我们来看这段代码:
js
const arr = (new Array(6)).fill(0);
const len = arr.length;
for (let i = 0;i<len;i++){
console.log(arr[i]);
}
在循环条件中直接使用 arr.length 会有微小的性能开销。
原因如下:
在 JavaScript 中,数组是一个特殊的对象。arr.length 不是一个简单的变量,而是一个访问器属性(Accessor Property) 。这意味着,每次你访问 arr.length 时,JavaScript 引擎实际上是在调用一个隐藏的函数来获取数组的长度。
在早期的 JavaScript 引擎中,这个函数调用的开销是比较明显的。如果在一个循环中(例如执行 10000 次)每次都去调用它,累积的开销就会变得可观。
遍历数组 foreach
我们接着说 forEach,你提到的两点 ------不能 break 和 性能相对较差------ 非常关键,我们来详细展开,并补充一些你可能忽略的细节。
1. forEach 基本用法
javascript
const arr = [1, 2, 3];
arr.forEach((item, index, array) => {
console.log(item, index, array);
});
-
回调参数:
item:当前遍历的元素(必选)index:当前元素的索引(可选)array:正在遍历的原数组(可选)
-
返回值 :
undefined(无论回调里写什么return,都不会改变这一点)
(1)无法中途退出
forEach 没有像 for 循环那样的 break 或 continue 机制。
-
想提前终止?做不到:
javascriptarr.forEach(item => { if (item === 2) break; // 报错:Illegal break statement }); -
想跳过当前元素?用
return模拟continue:javascriptarr.forEach(item => { if (item === 2) return; // 跳过当前迭代,进入下一次 console.log(item); // 输出 1, 3 });
map
javascript
运行
javascript
// map
// 作用:遍历数组,并对数组中的每个元素进行指定操作(加工/转换),
// 然后将所有操作结果收集起来,返回一个全新的数组。
// 核心:不改变原数组,返回一个长度相同、值经过转换的新数组。
// 适用场景:当你需要从一个数组"派生"出另一个结构相似但值不同的数组时。
const arr = [1, 2, 3, 4, 5, 6];
const newArr = arr.map(item => item + 1); // 对每个元素加 1
console.log(arr); // 输出: [1, 2, 3, 4, 5, 6] (原数组未改变)
console.log(newArr); // 输出: [2, 3, 4, 5, 6, 7] (新数组)
// 回调函数参数详解:
// item: 当前正在处理的数组元素 (必选)
// index: 当前元素在数组中的索引 (可选)
// array: 调用 map 方法的原数组本身 (可选)
const doubledWithIndex = arr.map((item, index) => {
return `Index ${index}: ${item * 2}`;
});
console.log(doubledWithIndex); // 输出: ["Index 0: 2", "Index 1: 4", "Index 2: 6", ...]
// 与 forEach 的区别:
// 1. forEach 仅用于遍历,没有返回值(返回 undefined)。
// 2. map 会返回一个新数组,更适合数据转换。
// 简单说:想"生成新数组"用 map,想"单纯遍历做操作"用 forEach。
for...of
javascript
运行
javascript
// for...of
// 作用:ES6 引入的一种新型循环语句,用于遍历可迭代对象(如 Array, Map, Set, String 等)。
// 核心:提供了一种更简洁、更具可读性的方式来遍历集合中的值。
// 优点:
// - 语法简洁,可读性远超传统 for 循环。
// - 避免了 for...in 循环遍历数组时可能遇到的原型链污染问题。
// - 可以与 break, continue, return 配合使用。
const arr = [1, 2, 3, 4, 5, 6];
// 基本用法
for (let item of arr) {
console.log(item); // 依次输出: 1, 2, 3, 4, 5, 6
}
// 如果需要索引,可以结合 Array.prototype.entries() 使用
for (let [index, item] of arr.entries()) {
console.log(`Index ${index}: ${item}`); // 依次输出: "Index 0: 1", "Index 1: 2", ...
}
// 与传统 for 循环对比
// 传统 for 循环:
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// for...of 写法更简洁,意图更明确。
// 与 for...in 对比 (注意:不推荐用 for...in 遍历数组)
// for...in 遍历的是键名(索引),且可能遍历到数组原型上的属性。
for (let key in arr) {
console.log(key); // 输出: "0", "1", "2", ... (字符串形式的索引)
}
总结对比
| 特性 / 方法 | map() |
for...of |
|---|---|---|
| 核心用途 | 数据转换,生成新数组。 | 遍历迭代,访问集合中的每个值。 |
| 返回值 | 返回一个新数组。 | 无返回值 (undefined)。 |
| 是否改变原数组 | 不改变(除非回调函数内手动修改)。 | 不改变(除非循环体内手动修改)。 |
| 适用对象 | 仅数组 (Array)。 |
所有可迭代对象 (Array, String, Map, Set, arguments 等)。 |
| 中断循环 | 无法中断(必须遍历完所有元素)。 | 可以使用 break 或 continue 中断或跳过。 |
| 获取索引 | 回调函数的第二个参数即为索引。 | 需要配合 arr.entries() 才能方便地获取索引。 |
何时使用?
- 使用
map:当你需要将一个数组的每个元素按照某种规则进行转换,最终得到一个新的数组时。例如,从 API 获取用户列表后,提取每个用户的id和name组成新数组。 - 使用
for...of:当你需要遍历一个数组或其他可迭代对象,对每个元素执行一些操作(如打印、累加、根据条件进行复杂处理等),并且可能需要中途停止循环时。它是日常遍历任务中替代传统for循环和forEach的优秀选择。