在JavaScript开发中,我们经常会遇到这样的需求:给定一个数组,想要知道里面是不是出现了某两个特定的元素紧挨着排列的情况。比如接口返回的状态码列表里,是否连续出现了「暂停」和「恢复」,或者用户输入的操作序列中是否包含了相邻的非法组合。这类问题表面看很简单,但写出来的代码在可读性和执行效率上差别很大。如果只考虑功能正确,新手往往会用嵌套循环去比对,而熟悉数组遍历机制的开发者则会用单次扫描来解决。

从原理上讲,JavaScript的数组本质上是一个带有长度属性和整数键的对象,但引擎层面通常以连续内存或稀疏结构优化存储。无论底层如何实现,我们通过下标访问元素的时间复杂度是常量级别。相邻元素意味着索引 i 与 i+1 处的元素需要满足某种条件。最直观的思路是,从索引0开始一直走到倒数第二个位置,每次拿出当前项和下一项做比较,一旦匹配就记录结果。这种方式只需要把数组走一遍,时间复杂度是 O(n),空间上只用了几个变量,属于非常轻量的操作。
不过很多人在初写这段代码时,会不自觉地写成两层循环:外层遍历每个元素,内层再向后扫描剩余元素来找配对。这种做法不仅把复杂度抬到了 O(n²),而且还会重复判断非相邻的情况,完全偏离了题意。我们需要明确,题目只关心「相邻」而非「存在」,所以内层循环是多余的设计。理解这一点,是做逻辑优化的第一步。
基础实现:单次遍历判断相邻对
最清晰的基础写法,就是利用 for 循环从 0 迭代到 arr.length - 2,在循环体里直接比对 arr[i] 和 arr[i+1]。下面这段代码演示了如何判断数组中是否出现了相邻的 'A' 和 'B'。
function hasAdjacentPair(arr, first, second) {
// 边界处理:数组长度小于2不可能有相邻对
if (arr.length < 2) {
return false;
}
// 单次遍历,i最大到倒数第二个元素
for (let i = 0; i < arr.length - 1; i++) {
if (arr[i] === first && arr[i + 1] === second) {
return true;
}
}
return false;
}
const logs = ['start', 'A', 'B', 'end'];
console.log(hasAdjacentPair(logs, 'A', 'B')); // 输出 true
上面的函数逻辑非常直白:只要发现一对相邻元素符合要求就立刻返回 true,如果走完都没遇到才返回 false。这种写法没有创建额外的数组或复杂结构,内存占用极低,在几千甚至几万长度的数组里都能瞬间跑完。对于只需要知道「是否出现」的场景,它已经足够好。
如果业务要求不是简单判断存在性,而是要找出所有满足条件的位置,那只要把 return true 改成向结果数组 push(i) 即可,循环结束后再返回结果数组。这样依然是一次遍历,不会破坏线性复杂度。需要注意的是,当数组里可能出现连续重叠的匹配时,比如 ['A','B','B'] 中找 'B','B',上面的写法会准确抓到索引1和2,不会漏掉,因为每次 i 只步进1。
逻辑优化:使用滑动窗口与前驱变量
当我们要判断的不是固定的一对,而是一组多种相邻规则,或者需要在遍历时同时做其他聚合统计,基础写法里的 arr[i+1] 取值虽然没问题,但可以用「前驱变量」让语义更清楚。所谓前驱变量,就是在循环开始前记录 prev,进入循环后当前项是 curr,判断 prev 与 curr 的关系,然后再把 curr 赋给 prev。这其实就是宽度为2的滑动窗口。
function findAllAdjacent(arr, first, second) {
const positions = [];
if (arr.length < 2) {
return positions;
}
let prev = arr[0];
// 从第二个元素开始,用 prev 和当前元素比对
for (let i = 1; i < arr.length; i++) {
const curr = arr[i];
if (prev === first && curr === second) {
positions.push(i - 1); // 记录前一项的索引
}
prev = curr;
}
return positions;
}
const data = ['x', 'A', 'B', 'A', 'B', 'y'];
console.log(findAllAdjacent(data, 'A', 'B')); // 输出 [1, 3]
这种写法的好处在于,循环内部不再出现 i+1 这样的偏移访问,降低了下标计算出错的概率。尤其在复杂逻辑中,你可能还要同时统计 prev 出现的次数或做类型转换,把「上一项」显式存下来会让代码更好维护。从性能角度看,它和基础写法几乎没有差别,因为都是 O(n) 且只访问每个元素一次。
如果相邻规则变多,例如同时要找 'A','B' 和 'B','C',可以把规则放进一个数组,在循环里用 some 或 for 匹配 [prev, curr] 是否在规则集中。此时滑动窗口结构依然成立,只是判断分支稍复杂。相比起为每种规则写一套独立循环,统一用前驱变量扫描一次明显更优,也避免了多次遍历数组带来的不必要开销。
边界情况与工程化封装建议
在实际项目里,数组可能来自接口或用户输入,类型并不总是一致的。比如数字 1 和字符串 '1' 在相邻判断时如果用 === 会判定为不相等,这可能符合预期,也可能造成bug。建议在封装函数时增加一个 strict 参数,当 strict 为 false 时使用 == 做宽松比对,并明确写进注释,避免调用方误解。
function hasAdjacent(arr, first, second, strict = true) {
if (!Array.isArray(arr) || arr.length < 2) {
return false;
}
const match = strict
? (a, b) => a === b
: (a, b) => a == b;
for (let i = 0; i < arr.length - 1; i++) {
if (match(arr[i], first) && match(arr[i + 1], second)) {
return true;
}
}
return false;
}
另一个常见边界是空位数组(sparse array),例如 [,'A','B'] 这种用逗号跳过元素的情况。在 for 循环里,空位会被当作 undefined 处理,如果你的业务不允许 undefined 参与匹配,应该在循环里加一层 arr[i] !== undefined 的过滤。此外,当数组特别巨大且只需知道「前几次出现」时,可以在找到足够数量匹配后提前 break,这也是一种逻辑上的短路优化。
最后从工程角度,建议把这类工具函数放进项目公共模块,并配上单元测试覆盖:正常相邻、首尾相邻、无匹配、重叠匹配、长度不足、类型不一致等用例。这样其他开发者在判断数组中特定相邻元素对的出现情况时,直接引用稳定函数即可,不必每次重复写循环,也减少了因个人写法差异引入的性能隐患和逻辑漏洞。
JavaScript数组相邻元素逻辑优化修改时间:2026-08-19 03:16:37