- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
本指南以现代 JavaScript 教程(en.javascript.info)中1-js/05-data-types章节的 数组去重任务 为主线,系统讲解如何利用Set集合实现任意类型数组的unique(arr)去重函数。读完本文,你将掌握Set的去重语义、Array.from与展开运算符的互转写法、复杂度对比,以及仓库测试用例的验证方法,并能将同一思路迁移到基于Map的分组去重场景。
任务背景与要求
该任务位于 Map and Set 章节 的练习部分,属于"数组去重(unique array members)"专项训练。题目原文要求如下:
给定数组
arr,编写函数unique(arr),返回一个由arr中唯一项组成的数组。
示例输入输出:
function unique(arr) { /* your code */ } let values = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O" ]; alert( unique(values) ); // Hare, Krishna, :-O题目附带两条关键提示:
- P.S. 数据类型不限于字符串:示例用的是字符串,但数组中可以存放任意类型的值;
- P.P.S. 要求使用
Set:这是本节练习的核心意图——用刚学过的Set集合来存储唯一值,而不是用filter+indexOf这类 O(n²) 的手工方案。
解题思路:为什么Set是天然的去重工具
在 Map and Set 章节 中,Set被定义为"无键值的特殊集合",每个值在集合中最多出现一次:
A
Setis a special type collection - "set of values" (without keys), where each value may occur only once.
其核心方法包括:
| 方法/属性 | 作用 |
|---|---|
new Set([iterable]) | 创建集合;传入可迭代对象(通常是数组)时,把其中的值复制进集合 |
set.add(value) | 添加值;若值已存在则什么也不做,并返回集合本身 |
set.has(value) | 返回true/false,判断值是否存在 |
set.delete(value) | 移除值,值存在时返回true |
set.clear() | 清空集合 |
set.size | 元素个数 |
"重复调用set.add(value)不产生任何效果"这一特性,正是去重任务的全部答案:把整个数组逐一add进Set,重复项会被自动过滤,剩下的就是唯一值集合。
官方参考解法:Array.from(new Set(arr))
仓库中该任务的官方参考实现位于 _js.view/solution.js,只有一行核心代码:
function unique(arr) { return Array.from(new Set(arr)); }其执行分两步:
new Set(arr):利用构造函数接受可迭代对象的特性,把数组元素一次性复制进集合,重复项被自动丢弃;Array.from(new Set(arr)):把Set(可迭代对象)转回数组返回。
一个更简洁的等价写法是展开运算符:
function unique(arr) { return [...new Set(arr)]; }两种方式语义完全一致,Array.from更贴近教程正文的讲授风格,展开运算符则常见于现代代码库。无论哪种写法,都具备两个重要性质:
- 保持首次出现顺序:
Set的迭代按插入顺序进行(教程总结中明确指出Map和Set的迭代始终遵循插入顺序),因此结果数组中每个唯一元素的位置等于它在原数组中的首次出现位置; - 不修改原数组:
Set只是复制引用/值,不会原地改写输入数组。
仓库测试用例:行为验证
该任务附带完整的 Mocha 测试套件 _js.view/test.js,用两个用例锁定了函数必须满足的行为:
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"]); }); });两个断言的解读:
assert.deepEqual(unique(strings), ["Hare", "Krishna", ":-O"]):不仅要求结果去重,还要求保持插入顺序——Hare在原数组中首次出现于索引 0,Krishna首次出现于索引 1,:-O最后,输出顺序与首次出现顺序严格一致;does not change the source array:调用unique后原数组不变,说明解法是纯函数式的,没有副作用。这正是new Set(arr)+Array.from组合优于arr.splice类原地改写的关键。
去重原理纵深:SameValueZero 与元素类型
Set判断值是否重复时,采用与Map相同的SameValueZero算法(ECMAScript 规范定义,不可自定义)。它与严格相等===几乎一致,唯一差异是NaN被视为等于自身。
这意味着直接套用本解法时:
NaN能被正确去重:
unique([NaN, NaN, 1]); // [NaN, 1](若用 indexOf 方案则会得到 [NaN, NaN, 1])- 对象按引用去重:两个内容相同但引用不同的对象属于不同元素,不会被合并:
let a = { id: 1 }, b = { id: 1 }; unique([a, b, a]); // [{ id: 1 }, { id: 1 }](两个不同引用 + 一个重复引用)- 基础类型值按值去重:
"Hare"与"Hare"、数字1与字符串"1"是不同元素(===语义),不会被混淆。
这也呼应了任务中"可以使用任意类型的值"的提示:解法对字符串、数字、NaN、布尔值、undefined、对象引用等所有类型一视同仁地按上述规则去重,无需任何类型分支。
复杂度对比:Set 方案 vs 手工方案
教程正文在介绍Set时给出了对比:若不使用Set,替代方案是用数组配合arr.find在每次插入时检查重复,但性能会差得多,因为该方法需要遍历整个数组逐一比对元素。对长度为 n 的数组:
| 方案 | 时间复杂度 | 说明 |
|---|---|---|
new Set(arr)+Array.from | O(n)(平均) | Set内部针对唯一性检查做了高度优化,插入与查询均为常数级(平均) |
filter+indexOf/ 循环 +find | O(n²) | 每个元素都要线性扫描已收集的部分 |
| 先排序再去重 | O(n log n) | 需额外排序步骤,且会破坏原始相对顺序 |
因此对真实数据集,Set方案在时间复杂度和代码简洁度上都是压倒性选择,这正是本任务强制要求使用Set的原因。
延伸练习:从 Set 去重到 Map 分组去重
同章节的下一个任务 Filter anagrams(过滤变位词) 是同一思路的进阶:若需要"按某个规范化键去重、并保留该组中的代表值",Set不够用,而Map正好胜任。其参考实现 _js.view/solution.js 展示了"排序字符串作为键"的技巧:
function aclean(arr) { let map = new Map(); for(let word of arr) { let sorted = word.toLowerCase().split("").sort().join(""); map.set(sorted, word); } return Array.from(map.values()); }该实现的配套测试 _js.view/test.js 验证了两点:每个变位词组只保留 1 个单词、且大小写不敏感(era与EAR视为一组)。这与unique任务形成了完美的对照学习链:
Set去重:值本身作为唯一性依据,适合"整值去重";Map分组去重:键(规范化结果)作为唯一性依据,值(组内代表)另行选择,适合"按映射键去重"。
常见陷阱与实战建议
- 不要用
map[key]风格访问:教程正文特别提醒,Map必须使用方法(set/get/has)而非下标赋值,后者会退化为普通对象语义。Set同理,务必使用add/has; - 对象去重先想清楚语义:
Set按引用去重,若想按对象的某个字段去重,需要Map+ 字段键(如变位词任务那样); - 保持顺序是特性不是巧合:
Set迭代按插入顺序进行,Array.from(new Set(arr))的结果天然保留"首次出现顺序",这是它优于"排序去重"方案的另一个原因; - 数值与字符串不混同:
1与"1"是不同的集合元素,涉及接口数据时需要留意隐式类型转换。
小结
本任务用一行Array.from(new Set(arr))完美诠释了Set的核心价值:O(n) 去重、保持首次出现顺序、不改动原数组、支持任意类型(含NaN与对象引用)。配套的 任务描述、参考解法 与 测试用例 组成了一套可运行、可验证的最小学习闭环;掌握它之后,再进入 变位词过滤任务 即可自然进阶到Map分组去重模式。
- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
相关推荐
JavaScript 数组去重实战:从 unique() 任务看 includes 循环与 Set 优化
JavaScript 数组去重实战:从 unique 任务看 includes 循环与 Set 优化 导读 本文基于 Modern JavaScript Tut
文档/教程前端en.javascript.info 实战任务解析:用 Array.map 把对象数组映射为 id + fullName 结构
en.javascript.info 实战任务解析:用 Array.map 把对象数组映射为 id + fullName 结构 本篇基于 Modern Java
文档/教程前端Apktool 如何把 APK 拆成可编辑文件
Apktool 如何把 APK 拆成可编辑文件 Apktool 是一个安卓 APK 逆向工具,一行命令把安装包拆成资源文件和 Smali 代码,改完再打包回 A
逆向工程开发工具移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考