同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
1. 开篇:有库可用,为什么还要自己写?
lodash、ramda 等库已经提供这些工具函数,但在面试、基础补强、和「读懂库源码」的场景里,手写一遍很有价值:
- 搞清概念:什么算「深拷贝」、什么算「去重」
- 踩一遍坑:循环引用、
NaN、Date、RegExp、Symbol等 - 形成习惯:知道什么时候用浅拷贝、什么时候必须深拷贝
下面按「深拷贝 → 去重 → 扁平化」的顺序,每种都给出可直接用的实现和说明。
2. 深拷贝
2.1 浅拷贝 vs 深拷贝,怎么选?
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 只改最外层、不改嵌套对象 | 浅拷贝({...obj}、Object.assign) | 实现简单、性能好 |
| 需要改嵌套对象且不想影响原数据 | 深拷贝 | 避免引用共享 |
对象里有 Date、RegExp、函数等 | 深拷贝时需特殊处理 | 否则会丢失类型或行为 |
一句话:只要会改到「嵌套对象/数组」,就考虑深拷贝。
2.2 常见坑
- 循环引用:
obj.a = obj,递归会栈溢出 - 特殊类型:
Date、RegExp、Map、Set、Symbol不能只靠遍历属性复制 - Symbol 做 key:
Object.keys不会包含,需用Reflect.ownKeys或Object.getOwnPropertySymbols
2.3 实现示例(含循环引用与特殊类型处理)
function deepClone(obj, cache = new WeakMap()) {
// 1. 基本类型、null、函数 直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 2. 循环引用:用 WeakMap 缓存已拷贝对象
if (cache.has(obj)) {
return cache.get(obj);
}
// 3. 特殊对象类型
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (obj instanceof Map) {
const mapCopy = new Map();
cache.set(obj, mapCopy);
obj.forEach((v, k) => mapCopy.set(deepClone(k, cache), deepClone(v, cache)));
return mapCopy;
}
if (obj instanceof Set) {
const setCopy = new Set();
cache.set(obj, setCopy);
obj.forEach(v => setCopy.add(deepClone(v, cache)));
return setCopy;
}
// 4. 普通对象 / 数组
const clone = Array.isArray(obj) ? [] : {};
cache.set(obj, clone);
// 包含 Symbol 作为 key
const keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)];
keys.forEach(key => {
clone[key] = deepClone(obj[key], cache);
});
return clone;
}
// 使用示例
const original = { a: 1, b: { c: 2 }, d: [3, 4] };
original.self = original; // 循环引用
const cloned = deepClone(original);
cloned.b.c = 999;
console.log(original.b.c); // 2,原对象未被修改
要点:WeakMap 解决循环引用,Date/RegExp/Map/Set 单独分支,Object.getOwnPropertySymbols 保证 Symbol key 不丢失。
3. 去重
3.1 场景与选型
| 场景 | 方法 | 说明 |
|---|---|---|
| 基本类型数组(数字、字符串) | Set | 写法简单、性能好 |
需要兼容 NaN | 自己写遍历逻辑 | NaN !== NaN,Set 能去重 NaN,但逻辑要显式写清楚 |
| 对象数组、按某字段去重 | Map 或 filter | 用唯一字段做 key |
3.2 几种实现
1)简单数组去重(含 NaN)
// 方式一:Set(ES6 最常用)
function uniqueBySet(arr) {
return [...new Set(arr)];
}
// 方式二:filter + indexOf(兼容性更好,但 NaN 会出问题)
function uniqueByFilter(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index);
}
// 方式三:兼容 NaN 的版本
function unique(arr) {
const result = [];
const seenNaN = false; // 用 flag 标记是否已经加入过 NaN
for (const item of arr) {
if (item !== item) { // NaN !== NaN
if (!seenNaN) {
result.push(item);
seenNaN = true; // 这里需要闭包,下面用修正版
}
} else if (!result.includes(item)) {
result.push(item);
}
}
return result;
}
// 修正:用变量
function uniqueWithNaN(arr) {
const result = [];
let hasNaN = false;
for (const item of arr) {
if (Number.isNaN(item)) {
if (!hasNaN) {
result.push(NaN);
hasNaN = true;
}
} else if (!result.includes(item)) {
result.push(item);
}
}
return result;
}
注意:Set 本身对 NaN 是去重的(ES2015 规范),所以 [...new Set([1, NaN, 2, NaN])] 结果正确。需要兼容 NaN 的,多是旧环境或面试题场景。
2)对象数组按某字段去重
function uniqueByKey(arr, key) {
const seen = new Map();
return arr.filter(item => {
const k = item[key];
if (seen.has(k)) return false;
seen.set(k, true);
return true;
});
}
// 使用
const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 1, name: '张三2' }
];
console.log(uniqueByKey(users, 'id'));
// [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]
4. 扁平化
4.1 场景
- 把
[1, [2, [3, 4]]]变成[1, 2, 3, 4] - 有时候需要「只扁平一层」或「扁平到指定层数」
4.2 实现
1)递归全扁平
function flatten(arr) {
const result = [];
for (const item of arr) {
if (Array.isArray(item)) {
result.push(...flatten(item));
} else {
result.push(item);
}
}
return result;
}
console.log(flatten([1, [2, [3, 4], 5]])); // [1, 2, 3, 4, 5]
2)指定深度扁平(如 Array.prototype.flat)
function flattenDepth(arr, depth = 1) {
if (depth <= 0) return arr;
const result = [];
for (const item of arr) {
if (Array.isArray(item) && depth > 0) {
result.push(...flattenDepth(item, depth - 1));
} else {
result.push(item);
}
}
return result;
}
console.log(flattenDepth([1, [2, [3, 4]]], 1)); // [1, 2, [3, 4]]
console.log(flattenDepth([1, [2, [3, 4]]], 2)); // [1, 2, 3, 4]
3)用 reduce 递归写法(另一种常见写法)
function flattenByReduce(arr) {
return arr.reduce((acc, cur) => {
return acc.concat(Array.isArray(cur) ? flattenByReduce(cur) : cur);
}, []);
}
5. 小结:日常怎么选
| 函数 | 生产环境 | 面试 / 巩固基础 |
|---|---|---|
| 深拷贝 | 优先用 structuredClone(支持循环引用)或 lodash cloneDeep | 自己实现,要处理循环引用和特殊类型 |
| 去重 | 基本类型用 [...new Set(arr)],对象用 Map 按 key 去重 | 要能解释 NaN、indexOf 等细节 |
| 扁平化 | 用原生 arr.flat(Infinity) | 手写递归或 reduce 版本 |
自己写一遍的价值在于:搞清楚边界情况、循环引用、特殊类型,以后选库或读源码时心里有数。
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~