先记住这个答案
数组 includes 通常按顺序查找,未命中或目标靠后时需要检查很多位置;Set 的规范要求平均访问时间优于线性,常见实现能提供很快的成员查询,但不强制所有实现每次都是 O(1)。对同一批较大数据进行多次查询,建立一次 Set 并复用往往值得;只查一次或每次都重建 Set,构建和内存成本可能抵消收益。还要确认两种数据结构的相等语义符合业务,并把更新同步、命中分布和目标引擎纳入测量,不能只跑一个 has 循环就给出普遍结论。
- 把索引构建和重复查询一起计入成本
- 规范要求平均次线性而非固定实现
- 查询收益依赖数据规模、命中位置与更新频率
先让两种实现完成相同工作
示例用一组允许编号过滤查询列表,数组版本每次 includes,集合版本先创建一个索引再 has。结果一致才有比较成本的意义,否则所谓更快可能只是漏掉了某些业务条件。
这里索引创建放在查询外部,表示允许集合在本轮查询中不变。如果把 new Set 放进 filter 回调,每个查询都重新读取整份输入,就失去了复用数据结构的主要价值。
const allowed = ['a', 'c', 'e'];
const queries = ['e', 'x', 'a', 'x'];
const scanned = queries.filter(value => allowed.includes(value));
const index = new Set(allowed);
const indexed = queries.filter(value => index.has(value));
console.log(JSON.stringify(scanned));
console.log(JSON.stringify(indexed));查看输出与解释
["e","a"]
["e","a"]两种方法保留相同查询项和顺序,Set 只构建一次。这个小输入用于验证语义,不足以说明实际速度差异,真实性能需要覆盖代表性规模与索引生命周期。
命中位置与构建频率会改变选择
数组查询经常命中第一项时,扫描可能很快结束;大量未命中则更容易付出完整扫描成本。小集合的额外分配和常数开销也可能占主导,不能只用最坏复杂度判断所有短数组。
若允许列表频繁更新,应比较增量维护 Set、整体重建和直接扫描的总成本。还需要决定两份结构由谁负责同步,源数组已修改而索引仍旧会产生错误结果,比少量性能差异更重要。
公平测量需要保留真实输入和输出用途
测量应包含冷启动或稳定阶段的实际比例、不同查询分布,以及结果是否真的被消费。只选择有利于某一种写法的输入,或把构建成本放在计时范围外,会得到难以应用的数字。
内存、垃圾回收和峰值延迟也要观察,尤其在服务请求里每次创建大集合的场景。最终选择应有可复现环境和数据规模说明,避免把单机一次短测试写成跨浏览器的固定性能倍率。
容易答错的地方
- 把 Set.has 每次 O1 当作规范承诺
- 规范约束是平均访问时间优于线性,允许不同内部数据结构。可以说明常见实现的预期效率,但不要承诺最坏情况或所有引擎实现,更不能用未经测量的倍率替代实际验证。
- 只测查询而遗漏反复构建集合
- 若真实路径每次都重新创建 Set,构建和分配可能是主要成本。应按真实生命周期测量整段工作,先确认索引能够复用,再讨论单次 has 与 includes 的差异。
面试官还会怎么问?
Set 与 includes 的 NaN 判断一样吗?
二者使用的相等语义都能识别 NaN,并把正负零视为相同,对象仍按身份。业务若按字段内容匹配,需要额外提取稳定键,不能因为查询结果类型相同就忽略身份模型。
只查询一次应该直接用 includes 吗?
通常可以先采用清楚的直接扫描,但仍取决于集合是否已经存在和输入规模。若上游本来就维护 Set,就没有额外构建成本;选择应围绕现有数据形态和真实查询需求。
同时需要顺序列表和快速查询怎么办?
可以维护数组与 Set 两种视图,但必须指定唯一更新入口或从同一版本生成,防止不一致。若数据规模很小,双结构维护复杂度未必值得,应结合实际热点和可维护性判断。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。