JavaScript:内存、垃圾回收、性能优化 第十二阶段内存、垃圾回收、性能优化JS 内存划分栈内存基本类型基本数据类型number、string、boolean、null、undefined、symbol、bigint的值直接存储在栈内存中。栈内存的特点是数据存储和访问速度快因为它遵循后进先出LIFO的原则。例如letnum10;letstrhello;letbooltrue;这里的num、str和bool都是基本类型它们的值直接存储在栈内存中。堆内存引用类型引用数据类型如Object、Array、Function等的值存储在堆内存中。在栈内存中只存储一个指向堆内存中实际数据的引用地址。例如letobj{name:John};letarr[1,2,3];functionfunc(){}对于obj、arr和func栈内存中存储的是指向堆内存中对应数据的地址实际的数据存储在堆内存中。这是因为引用类型的数据结构可能比较复杂需要动态分配内存空间堆内存更适合这种需求。垃圾回收机制 GC引用计数算法是什么引用计数算法为每个对象维护一个引用计数记录该对象被引用的次数。当一个对象的引用计数变为 0 时说明该对象不再被使用垃圾回收器会回收其占用的内存。例如当你创建一个对象并将其赋值给一个变量时该对象的引用计数加 1当变量被重新赋值或超出作用域时对象的引用计数减 1。缺点引用计数算法无法处理循环引用的问题。例如两个对象相互引用即使它们都不再被外部引用但它们的引用计数都不会为 0导致内存无法回收。因此现代 JavaScript 引擎主要使用标记清除算法而引用计数算法在一些场景下作为辅助手段。标记清除算法是什么这是 JavaScript 中最常用的垃圾回收算法。它的工作过程分为两个阶段标记阶段和清除阶段。在标记阶段垃圾回收器从根对象如全局对象window开始遍历所有可以访问到的对象并标记它们。然后在清除阶段垃圾回收器会遍历堆内存回收所有未被标记的对象所占用的内存空间。例如当一个对象不再被任何根对象或其他已标记对象引用时它就不会被标记从而在清除阶段被回收。为什么这种算法可以有效回收不再使用的对象内存避免内存泄漏。它不依赖于对象的引用计数能够处理循环引用的情况在引用计数算法中循环引用可能导致对象无法被回收。内存泄漏场景全局变量如果定义了全局变量且没有及时释放它会一直存在于内存中。例如// 全局变量letglobalVar;functioncreateGlobalVar(){globalVar{largeData:newArray(1000000).fill(1)};}createGlobalVar();// 即使createGlobalVar函数执行完毕globalVar及其数据仍占用内存闭包闭包可以访问外部函数作用域中的变量这可能导致这些变量在函数执行完毕后仍然无法被垃圾回收。例如functionouter(){letlargeObject{data:newArray(1000000).fill(1)};returnfunctioninner(){returnlargeObject.data[0];};}letinnerFuncouter();// 这里outer函数执行完毕但largeObject由于被闭包innerFunc引用无法被回收定时器未清除如果设置了定时器setTimeout或setInterval但没有及时清除定时器的回调函数及其依赖的变量会一直存在于内存中。例如letintervalIdsetInterval((){console.log(定时器运行);},1000);// 如果不调用clearInterval(intervalId)定时器及其回调会持续占用内存DOM 引用当 JavaScript 对象持有对 DOM 元素的引用而该 DOM 元素从文档中移除时如果没有清除这些引用相关对象和 DOM 元素占用的内存不会被回收。例如dividmyDiv/divscriptletdivdocument.getElementById(myDiv);letlargeObj{relatedDiv:div};// 假设myDiv从文档中移除但由于largeObj持有对它的引用相关内存无法回收/script事件未解绑如果为 DOM 元素或其他对象添加了事件监听器但在对象销毁或不再需要时没有解绑事件事件监听器及其相关的闭包会一直存在于内存中。例如buttonidmyButton点击/buttonscriptletbuttondocument.getElementById(myButton);functionhandleClick(){console.log(按钮点击);}button.addEventListener(click,handleClick);// 如果没有调用button.removeEventListener(click, handleClick)handleClick函数及其依赖会占用内存/script性能优化方向循环优化减少循环体中的计算将循环体中不依赖循环变量的计算提前到循环外部。例如// 优化前for(leti0;iarr.length;i){letresultcomplexCalculation();arr[i]result;}// 优化后letresultcomplexCalculation();for(leti0;iarr.length;i){arr[i]result;}使用更高效的循环方式对于数组for循环通常比forEach性能更好特别是在处理大数据量时。例如letarrnewArray(1000000).fill(1);// 使用for循环letsum0;for(leti0;iarr.length;i){sumarr[i];}// 使用forEach循环sum0;arr.forEach(numsumnum);// for循环在处理大数据量时性能更好减少 DOM 操作文档碎片 DocumentFragmentDOM 操作是比较昂贵的操作频繁操作 DOM 会导致性能下降。可以使用文档碎片来批量操作 DOM最后将文档碎片添加到 DOM 树中。例如ulidmyList/ulscriptletlistdocument.getElementById(myList);letfragmentdocument.createDocumentFragment();for(leti0;i10;i){letlidocument.createElement(li);li.textContent列表项${i};fragment.appendChild(li);}list.appendChild(fragment);/script防抖节流防抖Debounce在事件触发后等待一定时间例如 300 毫秒如果在这段时间内事件再次触发则重新计时。常用于输入框搜索、窗口 resize 等场景。例如inputtypetextidsearchInputscriptletinputdocument.getElementById(searchInput);functiondebounce(func,delay){lettimer;returnfunction(){letcontextthis;letargsarguments;clearTimeout(timer);timersetTimeout((){func.apply(context,args);},delay);};}functionsearch(){console.log(搜索:,input.value);}input.addEventListener(input,debounce(search,300));/script节流Throttle在事件持续触发的过程中每隔一定时间例如 100 毫秒执行一次回调函数。常用于滚动条滚动、鼠标移动等场景。例如dividmyDivstyleheight:200px;overflow:auto;内容/divscriptletdivdocument.getElementById(myDiv);functionthrottle(func,delay){letlastTime0;returnfunction(){letcontextthis;letargsarguments;letnownewDate().getTime();if(now-lastTimedelay){func.apply(context,args);lastTimenow;}};}functionhandleScroll(){console.log(滚动中);}div.addEventListener(scroll,throttle(handleScroll,100));/script虚拟列表在处理大量列表数据时只渲染可见区域的列表项通过滚动来动态更新可见区域的内容。例如在 React 中可以使用react- virtualized库来实现虚拟列表。这样可以显著减少 DOM 元素的数量提高性能。请求缓存对于频繁请求且数据变化不大的 API可以在前端缓存请求结果。例如使用localStorage或内存缓存来存储请求结果下次请求时先检查缓存中是否有数据如果有则直接使用缓存数据避免重复请求。例如functionfetchData(){letcachedDatalocalStorage.getItem(apiData);if(cachedData){returnJSON.parse(cachedData);}// 实际请求APIletdata{result:示例数据};localStorage.setItem(apiData,JSON.stringify(data));returndata;}避免深拷贝深拷贝会创建一个与原对象完全相同的新对象包括所有层级的嵌套对象和数组。这可能会消耗大量内存和时间。在一些情况下可以通过其他方式来实现类似的功能比如使用浅拷贝Object.assign或展开运算符或者直接操作原对象前提是不影响其他部分的逻辑。例如letobj1{a:{b:1}};// 浅拷贝letobj2{...obj1};// 这里obj2.a和obj1.a指向同一个对象并非深拷贝合理使用 WeakMap/WeakSet 弱引用WeakMap和WeakSet中的键或值是弱引用当对象没有其他引用时垃圾回收器可以回收这些对象即使它们在WeakMap或WeakSet中。这可以避免因对象引用导致的内存泄漏。例如在缓存 DOM 元素相关数据时可以使用WeakMapletweakMapnewWeakMap();letdivdocument.getElementById(myDiv);weakMap.set(div,{data:与div相关的数据});// 当div从文档中移除且没有其他引用时垃圾回收器可以回收div及其相关数据懒加载延迟加载页面中暂时不需要的资源如图片、脚本等。当用户滚动到相关区域或有实际需求时再加载。例如在 HTML 中可以使用loadinglazy属性来实现图片的懒加载imgsrcimage.jpgalt示例图片loadinglazy分块加载将大型脚本或样式文件分割成多个小块按需加载。例如在 Webpack 中可以使用代码分割功能来实现分块加载提高页面的加载速度。requestAnimationFrame 动画优化requestAnimationFrame会在浏览器下一次重绘之前调用传入的回调函数它会根据浏览器的刷新频率来调整动画的执行比使用setTimeout或setInterval实现动画更平滑性能更好。例如functionanimate(){// 动画逻辑requestAnimationFrame(animate);}animate();实战练习制造内存泄漏场景控制台 Memory 面板观察WeakMap 改造缓存自动释放对象DocumentFragment 批量插入 DOM 减少重绘回流思考题JS 垃圾回收什么时候触发WeakMap 键对象无引用后什么时候回收重绘和回流区别如何减少