JavaScript 内存与引用:深究深浅拷贝、垃圾回收与 WeakMap/WeakSet

张开发
2026/4/6 23:29:20 15 分钟阅读

分享文章

JavaScript 内存与引用:深究深浅拷贝、垃圾回收与 WeakMap/WeakSet
写好 JavaScript 不仅要理解作用域和原型链更要摸清数据在内存中的流转方式。本文将从深浅拷贝出发延伸到垃圾回收机制最后通过 WeakMap 与 WeakSet 揭示弱引用的巧妙设计——帮你打通 JavaScript 内存管理的“任督二脉”。前言你是否遇到过这样的 bug明明复制了一个对象修改副本却把原对象也改了或者写了一个长期运行的应用内存占用不断攀升最终页面卡死这些问题背后都指向同一个核心主题——JavaScript 的内存与引用。本文将围绕三个密切相关的话题展开深浅拷贝理解值传递与引用传递的本质差异。垃圾回收掌握引擎如何自动清理无用内存。WeakMap / WeakSet利用弱引用优化内存敏感的场景。三者看似独立实则环环相扣。让我们一起深入底层写出更健壮、更高效的代码。一、深浅拷贝复制背后的内存真相1.1 数据类型与内存存储JavaScript 数据类型分为两类原始类型string、number、boolean、null、undefined、symbol、bigint。它们直接存储在栈内存中赋值时复制的是“值”本身。引用类型object包括数组、函数、日期等。它们的实际数据存储在堆内存中变量保存的只是一个内存地址指针。leta42;// 栈中存值 42letba;// 将 42 复制给 b独立b100;console.log(a);// 42 ✅ 不受影响letobj1{name:Alice};letobj2obj1;// 复制的是地址obj2 和 obj1 指向同一块堆内存obj2.nameBob;console.log(obj1.name);// Bob ❌ 原对象被修改了这就是“浅拷贝”产生的根源——只复制了引用没有复制真正的对象。1.2 浅拷贝的实现与局限浅拷贝会创建一个新对象但只复制第一层属性。如果属性值是原始类型则互不影响如果属性值是引用类型则新旧对象共享该引用。常见浅拷贝方法// 展开运算符constcopy1{...original};// Object.assignconstcopy2Object.assign({},original);// 数组的 slice / concatconstarrCopyoriginalArr.slice();// 手写浅拷贝functionshallowClone(obj){constresultArray.isArray(obj)?[]:{};for(letkeyinobj){if(obj.hasOwnProperty(key)){result[key]obj[key];}}returnresult;}局限演示constuser{name:Alice,address:{city:Beijing,zip:100000}};constcopy{...user};copy.address.cityShanghai;console.log(user.address.city);// Shanghai ❌ 内部对象仍被共享1.3 深拷贝彻底分离深拷贝会递归复制所有层级的属性生成完全独立的对象。方法一JSON.parse(JSON.stringify(obj))常用但有坑constdeepCopyJSON.parse(JSON.stringify(user));局限性无法复制undefined、函数、Symbol。无法处理循环引用会报错。会丢失Date、RegExp、Map、Set等特殊对象的结构变成普通对象或字符串。constobj{fn:()console.log(hi),date:newDate()};constcopyJSON.parse(JSON.stringify(obj));console.log(copy);// { date: 2025-... } 函数丢失日期变字符串方法二递归实现处理基础类型 数组/普通对象functiondeepClone(value,hashnewWeakMap()){// 处理原始类型和 nullif(valuenull||typeofvalue!object)returnvalue;// 处理循环引用if(hash.has(value))returnhash.get(value);// 处理数组和对象constresultArray.isArray(value)?[]:{};hash.set(value,result);for(letkeyinvalue){if(value.hasOwnProperty(key)){result[key]deepClone(value[key],hash);}}returnresult;}方法三structuredClone现代浏览器的原生深拷贝constclonestructuredClone(original);支持大多数内置类型Date、RegExp、Map、Set、ArrayBuffer等也能处理循环引用。但不支持函数、Symbol、DOM 节点。选择建议日常简单数据用JSON方法复杂场景用structuredClone或成熟的库如 Lodash 的_.cloneDeep。二、垃圾回收引擎如何自动管理内存2.1 可达性概念JavaScript 的内存管理是自动的其核心思想是可达性从根对象如window、globalThis、执行栈中的变量出发能够通过引用链访问到的对象就是“可达”的不会被回收反之不可达的对象会被标记为垃圾择机清除。letobj{data:newArray(10000)};// obj 可达objnull;// 原先的对象没有引用了 → 变为不可达 → 等待回收2.2 垃圾回收算法标记清除Mark-Sweep现代 JavaScript 引擎V8、SpiderMonkey主要采用标记清除算法配合分代回收、增量标记等优化。标记阶段从根对象开始递归遍历所有可达对象并打上标记。清除阶段遍历堆内存将没有标记的对象内存释放。引用计数早期 IE 使用存在循环引用问题已不再作为主流方案functionleak(){leta{};letb{};a.refb;b.refa;// 互相引用计数永远不为 0 → 内存泄漏}2.3 常见内存泄漏场景即使有垃圾回收不当的代码仍会造成内存泄漏。意外的全局变量functionfoo(){barleak;// 未声明挂在全局}未清理的定时器或事件监听setInterval((){// 引用了 DOM 元素或其他大对象组件销毁后未清除定时器},1000);闭包持有大数组functionouter(){constbigDatanewArray(1000000);returnfunctioninner(){console.log(bigData.length);// inner 一直引用 bigData};}constfnouter();// bigData 无法释放离屏 DOM 引用letdetachedDivdocument.getElementById(removed);document.body.removeChild(detachedDiv);// detachedDiv 变量仍然引用该 DOM导致无法回收2.4 如何主动辅助垃圾回收将不再使用的变量赋值为null。使用WeakMap或WeakSet见下一节。在开发工具 Performance 面板中记录内存快照分析 retained size。三、WeakMap 与 WeakSet弱引用的智慧3.1 弱引用的含义弱引用不会阻止垃圾回收器回收一个对象。也就是说如果一个对象只被 WeakMap 或 WeakSet 引用而没有其他强引用那么它随时可能被回收。与之对应Map 和 Set 持有的是强引用——只要键/值还在 Map/Set 中对象就不会被回收。3.2 WeakMap 的特性与 API键必须是对象不能是原始值。键是弱引用值可以是任意类型。不可迭代没有size属性无法forEach。这确保了回收时机对外不可知。letobj{name:data};constwmnewWeakMap();wm.set(obj,some metadata);objnull;// 原始对象失去强引用// 下一次 GC 后wm 中的对应条目会自动消失3.3 典型应用场景场景一存储 DOM 元素的私有数据constelementDatanewWeakMap();document.querySelectorAll(.btn).forEach(btn{elementData.set(btn,{clicks:0});btn.addEventListener(click,(){constdataelementData.get(btn);data.clicks;console.log(Clicked${data.clicks}times);});});// 当 btn 被从 DOM 移除且无其他引用时关联的元数据会自动回收无需手动清理。场景二缓存计算结果避免内存膨胀constcachenewWeakMap();functionprocess(obj){if(!cache.has(obj)){constresult/* 昂贵计算 */obj.name processed;cache.set(obj,result);}returncache.get(obj);}// 当 obj 不再使用时缓存条目自动消失防止缓存无限增长。场景三保存私有字段结合闭包const_privatenewWeakMap();classPerson{constructor(name){_private.set(this,{name});}getName(){return_private.get(this).name;}}// 外部无法访问私有数据且 Person 实例销毁后私有数据自动回收3.4 WeakSet 简介WeakSet 的值只能是对象且弱引用。没有size、不可迭代。常用于标记对象是否“处理过”避免重复处理同时不影响垃圾回收。constprocessednewWeakSet();functionhandle(item){if(processed.has(item))return;processed.add(item);// 执行处理逻辑...}3.5 Map/Set 与 WeakMap/WeakSet 对比表特性Map / SetWeakMap / WeakSet键类型任意值必须是对象引用类型强引用弱引用可迭代是keys()等否size属性有无内存泄漏风险需手动删除自动避免前提是无强引用典型用途缓存、集合运算DOM 关联、私有数据、临时标记四、三者关联一张图串起内存管理┌─────────────────┐ ┌─────────────────┐ │ 深浅拷贝 │ │ 垃圾回收 │ │ ───────────── │ │ ──────────── │ │ • 值 vs 引用 │ │ • 可达性 │ │ • 浅拷贝共享 │ ──→ │ • 标记清除 │ │ • 深拷贝隔离 │ │ • 内存泄漏场景 │ └────────┬────────┘ └────────┬────────┘ │ │ │ (深拷贝断开引用链) │ (弱引用不阻止回收) │ │ ▼ ▼ ┌─────────────────────────────────────────┐ │ WeakMap / WeakSet │ │ ─────────────────────────────────── │ │ • 键对象弱引用配合 GC 自动回收 │ │ • 解决缓存/事件监听中的内存泄漏 │ └─────────────────────────────────────────┘深浅拷贝决定了对象之间是否共享引用——错误的拷贝方式可能导致意外的共享或性能开销。垃圾回收自动清理不可达对象但开发者需要避免“无意识”的强引用如全局变量、闭包。WeakMap / WeakSet提供了“自愿被回收”的引用方式是解决特定内存问题如 DOM 缓存、私有数据的利器。理解这三者你就能写出既符合逻辑、又对内存友好的 JavaScript 代码。结语从深浅拷贝的“引用陷阱”到垃圾回收的“自动幕后”再到 WeakMap/WeakSet 的“弱引用魔法”——JavaScript 的内存模型并不玄学它有着清晰的设计原则。希望这篇文章能帮助你在日常开发中正确选择拷贝方式避免副作用。主动排查内存泄漏提升应用稳定性。在合适的场景使用WeakMap/WeakSet写出更优雅、更高效的代码。内存管理是优秀前端工程师的分水岭之一。现在你已经拿到了跨越它的钥匙。立即进入

更多文章