- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
导读
本文围绕《现代 JavaScript 教程》中 "过滤数组中的唯一元素"(unique(arr))这一核心练习展开,系统讲解如何借助Set集合类型完成数组去重。你将掌握Set的完整 API 与迭代特性、Array.from与展开运算符两种官方推荐解法、基于filter/indexOf的传统实现及其性能差异,并通过仓库内的源码与测试用例验证每种方案的边界行为(包括NaN、对象引用与空数组)。读完本文,你不仅能独立写出健壮的去重函数,还能理解Set与Map在唯一性判断上的底层比较算法,为后续的实战编码打下基础。
任务概述:实现unique(arr)
在 任务描述 中,要求实现一个函数unique(arr):给定一个数组arr,返回一个由其中所有唯一元素组成的新数组。
function unique(arr) { /* 你的代码 */ } let values = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O" ]; alert( unique(values) ); // Hare, Krishna, :-O任务给出了两点关键提示:
- 元素类型不限于字符串:示例中虽然用的是
string类型,但唯一性过滤对任何类型的值都应成立,包括数字、布尔值、对象引用,甚至NaN。 - 使用
Set来存储唯一值:这正是本任务想训练的核心——Set天然保证"每个值只出现一次"。
前置知识:Set是什么
Set是 ES6 引入的特殊类型集合,即"值的集合"(没有键),它的每一个值只能出现一次。完整的 API 在 Map and Set 章节 中有系统讲解,核心成员如下:
new Set(iterable)—— 创建一个set;如果传入一个可迭代对象(通常是数组),会把其中的值复制进set。set.add(value)—— 添加一个值,返回set本身(可链式调用)。set.delete(value)—— 删除值;如果value在调用时存在,返回true,否则返回false。set.has(value)—— 如果value在set中,返回true,否则返回false。set.clear()—— 清空set。set.size—— 返回元素个数(注意:是属性,不是方法)。
Set最重要的特性是:重复使用同一个值调用set.add(value)不会产生任何变化。这一点恰好就是去重问题的核心诉求——把数组里的元素逐个add进Set,重复值会被自动忽略。
Set与"数组 + find"方案的性能差异
Set的替代方案可以是一个普通数组,每次插入时用arr.find检查是否已存在。但正如 Map and Set 章节 所指出的,这种方案性能很差,因为find需要遍历整个数组来检查每个元素,时间复杂度为 O(n²)。而Set内部对唯一性检查做了优化,整体复杂度为 O(n)。
与Map的对比:键值对 vs 唯一值
Set与Map是一对姊妹数据结构:
Map是带键的数据项集合,允许任何类型的键(包括对象),并提供map.set(key, value)、map.get(key)、map.has(key)、map.delete(key)、map.clear()与map.size等完整 API;Set是唯一值的集合,没有键,只关心"值是否重复"。
两者在迭代上也完全兼容:Set同样支持set.keys()、set.values()与set.entries()(后两者是为兼容Map而存在,entries()返回[value, value]),且迭代顺序都遵循插入顺序。这也解释了为什么在去重场景中,Set比Map更简洁——我们不需要把元素当作键来存储,只需收集唯一值即可。
官方解法:Array.from(new Set(arr))
仓库中 array-unique-map 任务的参考实现 给出了最简洁、最符合任务意图的解法:
function unique(arr) { return Array.from(new Set(arr)); }这短短一行完成了两步工作:
new Set(arr):利用Set的可迭代构造器,将数组中的元素逐个加入集合,重复值自动被忽略;Array.from(new Set(...)):由于Set本身是可迭代对象(iterable)而非数组,需要用Array.from把它转换为数组返回。
Array.from在这里不是可有可无的——它能把任何可迭代对象或类数组对象转换为真正的数组。这一点在 "迭代键"任务 中有更直接的体现:map.keys()返回的是可迭代对象而非数组,直接调用keys.push("more")会抛出Error: keys.push is not a function,必须先Array.from(map.keys())再操作。
let map = new Map(); map.set("name", "John"); let keys = Array.from(map.keys()); // 关键转换 keys.push("more"); alert(keys); // name, more同理,Set也支持for..of与forEach迭代,但若需要数组方法(如push、map、filter),同样要先经过Array.from。
多种可行的去重实现对比
除了官方解法,该问题在工程实践中还有多种常见写法,各有适用场景。以下实现均可在浏览器控制台或 Node.js 中直接运行验证。
解法一:Array.from(new Set(arr))(推荐)
function unique(arr) { return Array.from(new Set(arr)); }优点:代码极简、语义清晰、性能 O(n)、适用于任何类型的值(含NaN与对象引用)。注意:Set基于SameValueZero算法比较值相等性——它和严格等于===差不多,唯一区别是NaN被视为等于NaN。因此[NaN, NaN]去重后会得到[NaN],而不会像某些基于===的实现那样保留两个NaN。
解法二:展开运算符[...new Set(arr)]
function unique(arr) { return [...new Set(arr)]; }由于Set是可迭代对象,展开运算符同样能将其"摊开"进数组字面量。这与Array.from效果等价,且写法更短。二者的取舍在于:
Array.from更显式、可读性更好,且对类数组对象(如arguments、NodeList)同样有效;- 展开运算符只适用于可迭代对象,遇到类数组对象会失败。
解法三:filter+indexOf(经典但较慢)
function unique(arr) { return arr.filter((item, index) => arr.indexOf(item) === index); }原理:indexOf返回元素第一次出现的位置,只有当当前下标等于首次出现下标时,该元素才是首次亮相,从而保留下来。
局限:indexOf使用严格相等===比较,无法正确处理NaN——[NaN, NaN]会得到[NaN, NaN](两个都保留),因为indexOf(NaN)恒为-1。此外时间复杂度为 O(n²),大数组下性能较差。
解法四:forEach+includes(可读性好)
function unique(arr) { let result = []; arr.forEach(item => { if (!result.includes(item)) { result.push(item); } }); return result; }原理直观:遍历原数组,只有当前值不在结果数组中时才推入。与indexOf类似,includes同样基于SameValueZero算法(能识别NaN),但整体复杂度仍为 O(n²)。
各方案行为对照表
| 方案 | 写法 | 复杂度 | 能否处理NaN | 返回新数组(不改原数组) |
|---|---|---|---|---|
| Set + Array.from | Array.from(new Set(arr)) | O(n) | ✅(SameValueZero) | ✅ |
| Set + 展开运算符 | [...new Set(arr)] | O(n) | ✅(SameValueZero) | ✅ |
| filter + indexOf | arr.filter((v, i) => arr.indexOf(v) === i) | O(n²) | ❌(===比较) | ✅ |
| forEach + includes | 手动收集 | O(n²) | ✅(SameValueZero) | ✅ |
在 任务描述 的 P.S. 中明确建议"使用Set来存储唯一值",因此前两种方案更契合本任务的训练意图。
源码佐证:测试用例如何验证正确性
仓库中的 test.js 用 Mocha 框架从两个维度约束了unique的行为,任何合格的实现都必须通过:
describe("unique", function() { it("removes non-unique elements", function() { let strings = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O" ]; assert.deepEqual(unique(strings), ["Hare", "Krishna", ":-O"]); }); it("does not change the source array", function() { let strings = ["Krishna", "Krishna", "Hare", "Hare"]; unique(strings); assert.deepEqual(strings, ["Krishna", "Krishna", "Hare", "Hare"]); }); });这两个用例揭示了去重函数必须满足的两条不变量:
- 结果正确性:重复元素必须被移除,且保留顺序与首次出现顺序一致(
Set的插入顺序迭代天然保证这一点); - 无副作用:
unique不能修改原数组——Set构造函数只是"读取并复制"传入的可迭代对象,原数组保持不变。这一点也是Array.from(new Set(arr))优于某些原地修改写法的关键。
深度延伸:Set/Map在同类问题中的实战应用
"用集合做唯一性判定"的思路在本仓库中还有两个同系列练习,可以作为本任务的进阶延伸。
延伸一:用Map过滤字谜(anagrams)
在 过滤字谜任务 中,要求清除互为字谜的单词(相同字母、不同顺序,如nap与PAN)。参考实现(见 solution.js)的核心思路是:把每个单词拆成字母、排序后再拼接,作为Map的键,从而让所有字谜映射到同一把键:
function aclean(arr) { let map = new Map(); for (let word of arr) { // 将单词 split 成字母,排序,再 join 回来 let sorted = word.toLowerCase().split('').sort().join(''); map.set(sorted, word); // 同键覆盖,每组字谜只保留最后遇到的那个 } return Array.from(map.values()); } let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; alert( aclean(arr) ); // nap,teachers,ear(或 PAN,cheaters,era)这里map.set(sorted, word)的"覆盖"语义恰好充当了去重机制:同一键再次写入时,旧值被新值替换,每组字谜最终只保留一个单词;最后Array.from(map.values())把值迭代为数组返回——这与本任务Array.from(new Set(arr))的收尾方式如出一辙。测试用例(test.js)还额外验证了大小写不敏感(era与EAR视为同一字谜)以及每组字谜恰好保留一个。
延伸二:迭代对象与Array.from的转换
在 迭代键任务 中,map.keys()返回的是可迭代对象而不是数组,直接调用数组方法会报错。这提醒我们:凡是把Set/Map的迭代结果当作数组使用(push、map、filter、sort等)之前,都必须先用Array.from(或展开运算符)完成转换。这也是官方解法中选择Array.from(new Set(arr))而非直接返回Set的根本原因。
总结与自检清单
本任务以一行Array.from(new Set(arr))收尾,但背后承载的知识点值得完整沉淀:
Set天然去重:重复add同一值不会改变集合,set.size即唯一元素个数;Array.from的必要性:Set是可迭代对象而非数组,返回前必须显式转换;SameValueZero比较算法:Set用该算法判重,NaN被视为等于自身,且算法不可自定义;- 无副作用原则:
unique应返回新数组,不修改原数组(测试用例明确约束了这一点); - 复杂度意识:
Set方案 O(n),filter + indexOf方案 O(n²),大数据量下差距显著。
动手验证时,可直接在浏览器 DevTools 或 Node.js 中粘贴本节任一实现,并对照 test.js 的两条断言逐项检查;进阶读者可继续完成 过滤字谜 与 迭代键 两个兄弟任务,把Map/Set的键控去重与迭代转换能力一并打通。
- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
相关推荐
JavaScript 数组去重:从 includes 朴素解法到 Set 优化(zh.javascript.info 实战解析)
JavaScript 数组去重:从 includes 朴素解法到 Set 优化(zh.javascript.info 实战解析) 本篇文章围绕《现代 JavaS
文档教程前端JavaScript 数组去重:从 includes 朴素解法到 Set 优化——Modern JavaScript Tutorial 实战任务解析
JavaScript 数组去重:从 includes 朴素解法到 Set 优化——Modern JavaScript Tutorial 实战任务解析 数组去重是
文档教程前端30 seconds of code:使用 Set 与数组方法去除 JavaScript 数组重复值
30 seconds of code:使用 Set 与数组方法去除 JavaScript 数组重复值 本文聚焦 30 seconds of code 仓库中的《
教程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考