作为 Web 开发人员,您知道您编写的每一行代码都会对应用程序的性能产生影响吗?谈到 JavaScript,最需要关注的领域之一就是内存管理。
想一想,每次用户与您的网站交互时,他们都会创建新的对象、变量和函数。如果您不小心,这些对象可能会堆积起来,阻塞浏览器的内存并降低整个用户体验。这就像信息高速公路上的交通堵塞,一个令人沮丧的瓶颈,可以让用户望而却步。
但它不一定是这样的。凭借正确的知识和技术,您可以控制您的 JavaScript 内存并确保您的应用程序平稳高效地运行。
在今天的文章中,我们将探讨 JavaScript 内存管理的来龙去脉,包括内存泄漏的常见原因以及避免它们的策略。无论您是专业的还是新手JavaScript开发人员,您都会对如何编写精简、平均和快速的代码有更深入的了解。
JavaScript 引擎使用垃圾收集器来释放不再使用的内存。垃圾收集器的工作是识别并删除应用程序不再使用的对象。它通过持续监控代码中的对象和变量,并跟踪哪些对象和变量仍在被引用来实现这一点。一旦一个对象不再被使用,垃圾收集器将其标记为删除并释放它正在使用的内存。
垃圾收集器使用一种称为“标记和清除”的技术来管理内存。它首先标记所有仍在使用的对象,然后“扫过”堆并删除所有未标记的对象。这个过程会定期进行,并且在堆内存不足时进行,以确保应用程序的内存使用始终尽可能高效。
当谈到 JavaScript 中的内存时,有两个主要参与者:堆栈和堆。
堆栈用于存储仅在函数执行期间需要的数据。它快速高效,但容量有限。当一个函数被调用时,JavaScript 引擎将函数的变量和参数压入堆栈,当函数返回时,它再次将它们弹出。堆栈用于快速访问和快速内存管理。
另一方面,堆用于存储应用程序整个生命周期所需的数据。它比栈慢一点,组织性差一点,但容量大得多。堆用于存储对象、数组和其他需要多次访问的复杂数据结构。
您很清楚内存泄漏可能是一个偷偷摸摸的敌人,它会潜入您的应用程序并导致性能问题。通过了解内存泄漏的常见原因,您可以用战胜它们所需的知识武装自己。
内存泄漏的最常见原因之一是循环引用。当两个或多个对象相互引用时,就会发生这种情况,从而形成垃圾收集器无法破坏的循环。这可能会导致对象在不再需要后很长时间内仍保留在内存中。
这是示例:
复制
let object1 = {};let object2 = {};// create a circular reference between object1 and object2object1.next = object2;object2.prev = object1;// do something with object1 and object2// ...// set object1 and object2 to null to break the circular referenceobject1 = null;object2 = null;
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
在此示例中,我们创建了两个对象,object1 和 object2,并通过向它们添加 next 和 prev 属性在它们之间创建循环引用。
然后,我们将 object1 和 object2 设置为 null 以打破循环引用,但由于垃圾收集器无法打破循环引用,因此对象将在不再需要后很长时间内保留在内存中,从而导致内存泄漏。
为了避免这种类型的内存泄漏,我们可以使用一种称为“手动内存管理”的技术,通过使用 JavaScript 的 delete 关键字来删除创建循环引用的属性。
复制
delete object1.next;delete object2.prev;
1.
2.
避免此类内存泄漏的另一种方法是使用 WeakMap 和 WeakSet,它们允许您创建对对象和变量的弱引用,您可以在本文后面阅读有关此选项的更多信息。
内存泄漏的另一个常见原因是事件监听器,当您将事件侦听器附加到元素时,它会创建对侦听器函数的引用,该函数可以防止垃圾收集器释放元素使用的内存。如果在不再需要该元素时未删除侦听器函数,这可能会导致内存泄漏。
我们一起来看一个例子:
复制
let button = document.getElementById("my-button");// attach an event listener to the buttonbutton.addEventListener("click", function() { console.log("Button was clicked!"); });// do something with the button// ...// remove the button from the DOMbutton.parentNode.removeChild(button);
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
在此示例中,我们将事件侦听器附加到按钮元素,然后从 DOM 中删除该按钮。即使按钮元素不再存在于文档中,事件侦听器仍附加到它,这会创建对侦听器函数的引用,以防止垃圾收集器释放该元素使用的内存。如果在不再需要该元素时未删除侦听器函数,这可能会导致内存泄漏。
为避免此类内存泄漏,在不再需要该元素时删除事件侦听器很重要:
复制
button.removeEventListener("click", function() { console.log("Button was clicked!"); });
1.
2.
3.
另一种方法是使用 EventTarget.removeAllListeners() 方法删除所有已添加到特定事件目标的事件侦听器。
复制
button.removeAllListeners();
1.
内存泄漏的第三个常见原因是全局变量。当您创建全局变量时,可以从代码中的任何位置访问它,这使得很难确定何时不再需要它。这可能会导致变量在不再需要后很长时间仍保留在内存中。这是一个例子:
复制
// create a global variablelet myData = { largeArray: new Array(1000000).fill("some data"), id: 1};// do something with myData// ...// set myData to null to break the referencemyData = null;
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
在这个例子中,我们创建了一个全局变量 myData 并在其中存储了大量数据。
然后我们将 myData 设置为 null 以中断引用,但是由于该变量是全局变量,它仍然可以从您的代码中的任何位置访问,并且很难确定何时不再需要它,这会导致该变量在内存中保留很长时间 在不再需要它之后,导致内存泄漏。
为避免这种类型的内存泄漏,您可以使用“函数作用域”技术。它涉及创建一个函数并在该函数内声明变量,以便它们只能在函数范围内访问。这样,当不再需要该函数时,变量会自动被垃圾回收。
复制
function myFunction() { let myData = {largeArray: new Array(1000000).fill("some data"),id: 1 }; // do something with myData // ...}myFunction();
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
另一种方法是使用 JavaScript 的 let 和 const 代替 var,这允许您创建块范围的变量。用 let 和 const 声明的变量只能在定义它们的块内访问,并且当它们超出范围时将被自动垃圾收集。
复制
{ let myData = {largeArray: new Array(1000000).fill("some data"),id: 1 }; // do something with myData // ...}
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
JavaScript 提供了内存管理工具和技术,可以帮助您控制应用程序的内存使用情况。
JavaScript 中最强大的内存管理工具之一是 WeakMap 和 WeakSet。这些是特殊的数据结构,允许您创建对对象和变量的弱引用。
弱引用不同于常规引用,因为它们不会阻止垃圾收集器释放对象使用的内存。这使它们成为避免循环引用引起的内存泄漏的好工具。这是一个例子:
复制
let object1 = {};let object2 = {};// create a WeakMaplet weakMap = new WeakMap();// create a circular reference by adding object1 to the WeakMap// and then adding the WeakMap to object1weakMap.set(object1, "some data");object1.weakMap = weakMap;// create a WeakSet and add object2 to itlet weakSet = new WeakSet();weakSet.add(object2);// in this case, the garbage collector will be able to free up the memory// used by object1 and object2, since the references to them are weak
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
在这个例子中,我们创建了两个对象,object1 和 object2,并通过将它们分别添加到 WeakMap 和 WeakSet 来创建它们之间的循环引用。
因为对这些对象的引用很弱,垃圾收集器将能够释放它们使用的内存,即使它们仍在被引用。这有助于防止循环引用引起的内存泄漏。
另一种内存管理技术是使用垃圾收集器 API,它允许您手动触发垃圾收集并获取有关堆当前状态的信息。
这对于调试内存泄漏和性能问题很有用。
以下是一个例子:
复制
let object1 = {};let object2 = {};// create a circular reference between object1 and object2object1.next = object2;object2.prev = object1;// manually trigger garbage collectiongc();
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
在此示例中,我们创建了两个对象,object1 和 object2,并通过向它们添加 next 和 prev 属性在它们之间创建循环引用。然后,我们使用 gc() 函数手动触发垃圾收集,这将释放对象使用的内存,即使它们仍在被引用。
请务必注意,并非所有 JavaScript 引擎都支持 gc() 函数,其行为也可能因引擎而异。还需要注意的是,手动触发垃圾回收会对性能产生影响,因此,建议谨慎使用,仅在必要时使用。
除了 gc() 函数,JavaScript 还为一些 JavaScript 引擎提供了 global.gc() 和 global.gc() 函数,也为一些浏览器引擎提供了 performance.gc() ,可以用来检查 堆的当前状态并测量垃圾收集过程的性能。
JavaScript 还提供堆快照和分析器,可以帮助您了解您的应用程序如何使用内存。堆快照允许您拍摄堆当前状态的快照并对其进行分析以查看哪些对象使用的内存最多。
下面是一个示例,说明如何使用堆快照来识别应用程序中的内存泄漏:
复制
// Start a heap snapshotlet snapshot1 = performance.heapSnapshot();// Do some actions that might cause memory leaksfor (let i = 0; i < 100000; i++) { myArray.push({largeData: new Array(1000000).fill("some data"), id: i }); }// Take another heap snapshotlet snapshot2 = performance.heapSnapshot();// Compare the two snapshots to see which objects were createdlet diff = snapshot2.compare(snapshot1);// Analyze the diff to see which objects are using the most memorydiff.forEach(function(item) { if (item.size > 1000000) {console.log(item.name); } });
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
在此示例中,我们在执行将大数据推送到数组的循环之前和之后拍摄两个堆快照,然后,比较这两个快照以识别在循环期间创建的对象。
接着,我们可以分析差异以查看哪些对象使用了最多的内存,这可以帮助我们识别由大数据引起的内存泄漏。
分析器允许您跟踪应用程序的性能并识别内存使用率高的区域:
复制
let profiler = new Profiler();profiler.start();// do some actions that might cause memory leaksfor (let i = 0; i < 100000; i++) { myArray.push({largeData: new Array(1000000).fill("some data"), id: i }); }profiler.stop();let report = profiler.report();// analyze the report to identify areas where memory usage is highfor (let func of report) { if (func.memory > 1000000) {console.log(func.name); } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
在这个例子中,我们使用 JavaScript 分析器来开始和停止跟踪我们应用程序的性能。该报告将显示有关已调用函数的信息以及每个函数的内存使用情况。
并非所有 JavaScript 引擎和浏览器都支持堆快照和分析器,因此在您的应用程序中使用它们之前检查兼容性很重要。
我们已经介绍了 JavaScript 内存管理的基础知识,包括垃圾回收过程、不同类型的内存以及 JavaScript 中可用的内存管理工具和技术。我们还讨论了内存泄漏的常见原因,并提供了如何避免它们的示例。
通过花时间了解和实施这些内存管理最佳实践,您将能够创建消除内存泄漏可能性的应用程序。