本文已获得原作者Ahmadshaded授权翻译。大多数时候,我们也只是在对内存管理一无所知的情况下进行开发,因为JS引擎为我们处理了这个问题。然而,有时我们会遇到内存泄漏等问题,只有了解内存分配的工作原理才能解决这些问题。在本文中,我将介绍内存分配和垃圾收集的工作原理以及如何避免一些常见的内存泄漏。缓存(内存)生命周期在JS中,当我们创建一个变量、函数或者任何对象时,JS引擎都会为它分配内存,当不再需要时释放。分配内存是在内存中保留空间的过程,而释放内存则是释放空间,准备用于其他目的。每次我们分配一个变量或创建一个函数时,该变量的存储都会经历以下相同的阶段:分配内存JS为我们处理了这个:它分配了我们创建对象所需的内存。使用内存使用内存是我们在代码中明确做的事情:读取和写入内存实际上就是读取和写入变量。释放内存这一步也由JS引擎处理,一旦分配的内存被释放,它就可以用于新的目的。内存管理上下文中的“对象”不仅包括JS对象,还包括函数和函数作用域。内存堆和栈现在我们知道,对于我们在JS中定义的所有内容,引擎都会分配内存并在不再需要时释放它。我想到的下一个问题是:这些东西将存储在哪里?JS引擎可以存储数据的地方有两个:堆和栈。堆和栈是引擎用于不同目的的两种数据结构。栈:静态内存分配栈是JS用来存储静态数据的一种数据结构。静态数据是引擎在编译时知道其大小的数据。在JS中,这包括原始值(字符串、数字、布尔值、未定义和null)和指向对象和函数的引用类型。由于引擎知道大小不会改变,它会为每个值分配固定数量的内存。在执行之前立即分配内存的过程称为静态内存分配。这些值和整个堆栈的限制是依赖于浏览器的。堆:动态内存分配堆是JS存储对象和函数的另一个数据存储空间。与栈不同的是,JS引擎不会为这些对象分配固定数量的内存,而是根据需要分配空间。这种分配内存的方式也称为动态内存分配。下面将比较这两种存储的特点:栈堆存储基本类型和引用。大小在编译时已知。在运行时分配固定数量的内存对象和函数。图像。constperson={name:'John',age:24,};JS在堆中为这个对象分配内存。实际值仍然是原始值,这就是它们存储在堆栈中的原因。consthobbies=['远足','阅读'];数组也是对象,这就是它们存储在堆上的原因。letname='John';//为字符串分配内存constage=24;//为单词分配内存name='JohnDoe';//为新字符串分配内存constfirstName=name.slice(0,4);//分配内存对于一个新的字符串,初始值是不可变的,所以JS不会改变原来的值,而是创建一个新的。JavaScript中的引用所有变量都先指向栈。在非原始值的情况下,堆栈包含对堆中对象的引用。堆的内存没有以任何特定方式排序,因此我们需要在堆栈上保留对它的引用。我们可以将引用视为地址,将堆中的对象视为这些地址所属的房屋。请记住,JS将对象和函数存储在堆上。原始类型和引用存储在堆栈中。在这张图中,我们可以观察到不同的值是如何存储的。注意person和newPerson是如何指向同一个对象的。caseconstperson={name:'John',age:24,};这将在堆上创建一个新对象,并在堆栈上创建对该对象的引用。垃圾回收现在,我们知道了JS是如何为各种对象分配内存的,但是在内存生命周期中,还有最后一步:释放内存。就像内存分配一样,JavaScript引擎为我们处理了这一步。更具体地说,垃圾收集器负责处理这个问题。一旦JS引擎识别出不再需要某个变量或函数,它就会释放它占用的内存。这样做的主要问题是无法确定是否仍然需要一些内存,这意味着不可能有一种算法在不再需要的时候立即收集所有不再需要的内存。一些算法可以很好地解决这个问题。在本节中,我将讨论最常用的方法:引用计数和标记清除算法。引用计数当一个变量被声明并且引用类型的值赋给该变量时,这个值的引用计数为1。如果相同的值赋给另一个变量,则该值的引用次数加1。反之,如果包含对该值的引用的变量采用另一个值,则该值的引用计数减一。当这个值的引用次数变为0时,就意味着没有办法访问这个值,那么它占用的内存空间就可以回收了。这样,下次垃圾收集器运行时,就会释放引用计数为零的值占用的内存。让我们看看下面的例子。请注意,在最后一帧中,堆上只剩下兴趣爱好,因为最后一次引用是对象。循环数引用计数算法的问题在于它不考虑循环引用。当一个或多个对象相互引用但不能再通过代码访问它们时,就会发生这种情况。letson={name:'John',};letdad={name:'Johnson',}son.dad=dad;dad.son=son;son=null;dad=null;由于父对象相互引用,算法分配的内存没有被释放,我们不能再访问这两个对象。将它们设置为null不会使引用计数算法识别出它们不再被使用,因为它们都有传入引用。mark-sweep算法有一个循环依赖的解决方案。它检测它们是否可以从根对象访问,而不是简单地计算对给定对象的引用。浏览器的根是window对象,而NodeJS中的根是全局的。该算法将无法访问的对象标记为垃圾,然后扫描(收集)它们。永远不会收集根对象。这样,循环依赖就不再是问题了。在前面的例子中,无论是dad对象还是son对象都不能从根访问。因此,它们都会被标记为垃圾并被收集。这个算法从2012年开始在所有现代浏览器中都实现了。只是性能和实现有所提升,算法的核心思想还是一样的。妥协自动垃圾收集使我们能够专注于构建应用程序,而不是将时间浪费在内存管理上。然而,我们需要权衡。内存使用由于算法无法准确知道何时不再需要内存,因此JS应用程序可能使用比实际需要更多的内存。即使对象被标记为垃圾,垃圾收集器也可以决定何时以及是否收集分配的内存。如果您希望您的应用程序的内存效率尽可能高,最好使用低级语言。但请记住,需要权衡取舍。性能垃圾收集算法通常会定期运行以清理未使用的对象。问题是我们开发人员不知道回收何时发生。大型垃圾回收或频繁的垃圾回收会影响性能。然而,这种影响通常不会被用户或开发人员注意到。内存泄漏将数据存储在全局变量中,最常见的内存问题可能就是内存泄漏。在浏览器的JS中,如果省略了var、const或let,变量将被添加到window对象中。用户=getUsers();这在严格模式下是可以避免的。除了不小心把变量添加到根目录外,很多时候我们需要这样使用全局变量,但是记得一旦用不到就手动释放。释放它就像给它null一样简单。window.users=null;忘记定时器和回调忘记定时器和回调会增加我们应用程序的内存使用。特别是在单页应用程序(SPA)中,动态添加事件侦听器和回调时必须小心。忘记定时器constobject={};constintervalId=setInterval(function(){//此处使用的所有内容在清除`setInterval`doSomething(object);},2000);上面的代码每2秒运行一次函数。如果我们的项目中有这样的代码,我们很有可能不需要一直运行它。只要不取消setInterval,引用的对象就不会被垃圾回收。确保在不再需要时清除它。clearInterval(intervalId);被遗忘的回调假设我们向按钮添加了一个onclick侦听器,之后按钮将被删除。较旧的浏览器无法收集听众,但如今,这已不再是问题。不过,当我们不再需要事件侦听器时,删除事件侦听器是一个好习惯。constelement=document.getElementById('button');constonClick=()=>alert('hi');element.addEventListener('click',onClick);element.removeEventListener('click',onClick);element.parentNode.removeChild(元素);离开DOM引用内存泄漏与前面的内存泄漏类似:它发生在用JS存储DOM元素时。constelements=[];constelement=document.getElementById('button');elements.push(element);functionremoveAllElements(){elements.forEach((item)=>{document.body.removeChild(document.getElementById(item.id)))});}在移除这些元素时,我们还需要确保该元素也从数组中移除。否则,将不会收集这些DOM元素。constelements=[];constelement=document.getElementById('button');elements.push(element);functionremoveAllElements(){elements.forEach((item,index)=>{document.body.removeChild(document.getElementById(item.id));elements.splice(index,1);});}由于每个DOM元素还保留对其父元素的引用,这可以防止垃圾收集器收集元素的父元素和子元素。总结在这篇文章中,我们总结了JS中内存管理的核心概念。写这篇文章可以帮助我们理清一些我们没有完全理解的概念。希望这篇文章对你有所帮助,下次再见,记得来一趟!作者:Ahmadshaded译者:前端小智微信公众号“大千世界”,可通过以下二维码关注。转载本文请联系大千世界公众号。
