手机版

JavaScript还谈到了内存优化

时间:2021-11-07 来源:互联网 编辑:宝哥软件园 浏览:

与C/C相比,我们使用的JavaScript的内存处理让我们在开发中更加注重业务逻辑的编写。然而,随着业务复杂度的不断增加,单页应用、移动HTML5应用和Node.js程序等的开发不断增加。JavaScript中的内存问题导致卡顿和内存溢出就不再奇怪了。本文将从JavaScript的语言层面讨论内存的使用和优化。从熟悉或者略有耳闻的方面,到大多数人不会注意到的地方,我们会一一分析。1.语言级内存管理1.1范围是JavaScript编程中非常重要的操作机制。在同步JavaScript编程中,它不能完全吸引初学者的注意力,但在异步编程中,良好的范围控制技能已经成为JavaScript开发人员的必备技能。此外,范围在JavaScript内存管理中起着至关重要的作用。在JavaScript中,有函数调用,语句和全局作用域可以形成作用域。以下面的代码为例:复制代码如下: varfoo=function(){ varlocal={ };};foo();console.log(本地);//=undefinedvar bar=function(){ local={ };};bar();console.log(本地);//={}这里我们定义了foo()函数和bar()函数,它们的目的是定义一个名为local的变量。但是最后的结果却大不相同。在foo()函数中,我们使用var语句来声明和定义一个局部变量,由于函数体内会形成一个作用域,所以这个变量会在作用域中定义。而且,foo()函数不做任何范围扩展处理,所以在函数执行后局部变量被破坏。无法在外部范围内访问该变量。在bar()函数中,局部变量不是用var语句声明的,而是直接定义为全局变量。因此,外部范围可以访问这个变量。复制代码如下: local={ };//这里的定义相当于global . local={ };1.2范围链在JavaScript编程中,你会遇到多层函数嵌套的场景,这就是范围链的典型表现。如下代码所示:复制代码如下: function foo(){ var val=' hello ';function bar(){ function baz(){ global . val=' world;} baz();console . log(val);//=hello } bar();} foo();根据前面对范围的描述,你可能认为这里代码显示的结果是world,但实际结果是hello。许多初学者在这里会开始感到困惑,所以让我们看看这段代码是如何工作的。因为在JavaScript中,变量标识符的搜索从当前范围开始,一直到全局范围。因此,在JavaScript代码中对变量的访问只能向外进行,而不能反过来。

baz()函数的执行在全局范围内定义了一个全局变量val。而在bar()函数中,在访问标识符val时,遵循从内到外的ed搜索原则:如果在bar函数的作用域中没有找到,则在下一级的foo()函数的作用域中进行搜索。但是,让大家困惑的关键就在这里:这个标识符访问在foo()函数的作用域中找到了一个匹配的变量,所以不会继续往外看,所以baz()函数中定义的全局变量val对这个变量访问没有影响。1.3闭包我们知道JavaScript中的标识符搜索遵循从内到外的原则。然而,由于业务逻辑的复杂性,单一的交付序列远远不能满足日益增长的新需求。我们来看看下面的代码:复制代码如下: function foo(){ var local=' hello ';return函数(){返回本地;};} var bar=foo();console . log(bar());//=Hello这里显示的外部作用域访问内部作用域的技术是闭包。由于高阶函数的应用,foo()函数的范围得到了扩展。foo()函数返回一个匿名函数,该函数存在于foo()函数的作用域中,因此可以访问foo()函数作用域中的局部变量并保存其引用。由于这个函数直接返回局部变量,所以bar()函数可以直接在外部范围内执行,以获得局部变量。闭包是JavaScript的一个高级特性,我们可以用它来实现更复杂的效果,以满足不同的需求。但是需要注意的是,因为带有内部变量引用的函数是从函数中带出的,所以在函数执行后,作用域中的变量不一定会被破坏,直到所有内部变量引用都被释放。因此,闭包的应用很容易导致无法释放内存的情况。2.JavaScript的内存回收机制在这里,我将以Chrome和Node.js使用的、Google介绍的V8引擎为例,简单介绍一下JavaScript的内存回收机制。更详细的内容可以买好朋友朴灵的书《深入浅出Node.js》学习,其中“记忆控制”一章有非常详细的介绍。在V8中,所有的JavaScript对象都是通过堆来分配的。

当我们在代码中声明一个变量并赋值时,V8将为这个变量分配一部分堆内存。如果请求的内存不足以存储该变量,V8将继续请求内存,直到堆大小达到V8的内存上限。默认情况下,V8的堆内存最大大小在64位系统是1464MB,在32位系统是732MB,大概是1.4GB和0.7GB,另外V8管理的是不同代的堆内存中的JavaScript对象:新一代和老一代。即新一代中生存期短的JavaScript对象,如临时变量、字符串;较老的一代是在许多垃圾收集中幸存下来的对象,并且具有较长的生命周期,例如主控制器和服务器对象。垃圾收集算法一直是编程语言研发中的重要一环,V8中使用的垃圾收集算法主要包括以下几个方面:1。Scavange算法:通过复制进行内存空间管理,主要用于新一代内存空间;2.Mark-Sweep算法和Mark-Compact算法:堆内存通过标记进行排序和回收,主要用于老一代对象的检查和回收。更详细的PS:的V8垃圾收集实现,可以通过阅读相关书籍、文档和源代码来了解。让我们看看在什么情况下JavaScript引擎会回收哪些对象。2.1作用域和引用初学者往往误认为函数执行时,函数内部声明的对象会被破坏。但事实上,这种理解并不严谨和全面,很容易导致混乱。引用是JavaScript编程中一个非常重要的机制,但奇怪的是,大多数开发人员并没有注意到它,甚至没有理解它。引用指的是“代码访问对象”的抽象关系,有点类似C/C的指针,但不是一回事。引用也是JavaScript引擎在垃圾收集中最关键的机制。以下面的代码为例:复制代码如下://.var val=' hello worldfunction foo(){ return function(){ return val;};} global . bar=foo();//.读完这段代码,你能说出这段代码执行后哪些对象还活着吗?根据相关原则,本规范中未回收的对象为val和bar()。是什么原因让它们无法回收?JavaScript引擎如何做垃圾收集?上面提到的垃圾收集算法只是用于收集,那么它如何知道哪些对象可以被收集,哪些对象需要生存呢?答案是引用了一个JavaScript对象。在JavaScript代码中,即使您简单地将变量名写成一行而不做任何事情,JavaScript引擎也会认为这是对对象的访问行为,并且存在对对象的引用。为了保证垃圾收集不影响程序逻辑的运行,JavaScript引擎绝不能回收正在使用的对象,否则会混淆。因此,判断一个对象是否在使用的标准是是否还有对该对象的引用。但实际上,这是一种妥协,因为JavaScript引用是可以转移的,所以有可能有些引用已经被带到了全局范围。然而,事实上,在业务逻辑中不再需要访问它们,应该被回收。但是,JavaScript引擎仍然会认为程序仍然需要它。如何正确使用变量和引用,是从语言层面优化JavaScript的关键。3.优化您的JavaScript终于进入了这个行业。非常感谢大家耐心看完。经过上面这么多的介绍,相信你对JavaScript的内存管理机制已经有了很好的了解,所以下面的技巧会让你更加优秀。3.1善用函数如果你有阅读优秀JavaScript项目的习惯,你会发现很多Daniel在开发前端JavaScript代码时,经常使用一个匿名函数来包装代码的最外层。

复制代码如下:(函数(){//主营业务代码})();有的更高级:复制代码如下:(函数(win、doc、$、undefined){//主营业务代码})(窗口、文档、jQuery);甚至前端模块化加载解决方案,如RequireJS、SeaJS、OzJS等。采用类似形式:复制的代码如下://require js definite([' jquery '],function($){//主营业务代码});//seajsdefine(' m module ',['dep ','下划线'],function ($,_){//main business code });如果你说Node.js开源项目的很多代码都不是这样处理的,那你就错了。在实际运行代码之前,Node.js将包装每一个。js文件转换成如下形式:复制的代码如下:(函数(exports,require,module,_ _ dirname,_ _ filename){//main business code });这样做有什么好处?众所周知,正如我们在文章开头所说的,在JavaScript中,有函数调用,有语句和全局范围。我们还知道,在全局范围内定义的对象可能会一直存在,直到进程退出。如果是很大的物体,那就麻烦了。比如有人喜欢用JavaScript做模板渲染:复制代码如下:php $ db=mysqli _ connect(服务器、用户、密码,‘myapp’);$ topics=MySQL _ query($ db,' SELECT * FROM topics');doctype html html lang=' en ' head meta charset=' utf-8 ' title你是猴子邀请的漫画吗?/title/head body ul id=' topics '/ul script type=' text/tmpl ' id=' topic-tmpl ' Li h1 %=title %/h1 p %=content %/p/Li/script script type=' text/JavaScript ' var data=?PHP echo JSON _ encode($ topics);var topic mpl=document . queryselector(' # topic-tmpl ')。innerHTMLvar render=function(tmlp,view){ var compiled=tmlp。替换(/\n/g,' \\n ')。替换(/%=([\s\S]?)%/g,函数(match,code){ return ' ' escape(' code ')' ';});符合=[' var RES=' ';'“,with (view || {}) {”,res=“”符合“”、“}”、“返回res”].联接(' \ n ');var fn=新功能('视图',已编译);返回fn(视图);};var topics=document . query select or(' # topics ');function init()data . foreach(function(topic){ topics . innerhtml=render(topic mpl,topic);});} init();/script/body/html这类代码在新手作品中经常可以看到。这里有什么问题?如果从数据库中获取的数据量非常大,那么在前端完成模板渲染之后,数据变量就会闲置。但是因为这个变量是在全局范围内定义的,所以JavaScript引擎不会回收和销毁它。这个变量将一直存在于旧的堆内存中,直到页面关闭。但是,如果我们做一些简单的修改,在逻辑代码中包装一层函数,效果会大不相同。UI渲染完成后,释放代码对数据的引用,执行最外层函数时,JavaScript引擎开始检查其中的对象,数据可以回收。3.2永远不要定义全局变量。正如我们刚才提到的,当一个变量在全局范围内定义时,默认情况下,JavaScript引擎不会回收和销毁它。这个变量将一直存在于旧的堆内存中,直到页面关闭。然后我们总是遵循一个原则:永远不要使用全局变量。虽然开发全局变量真的很容易,但是全局变量带来的问题远比它们带来的便利更严重。使变量难以恢复;1.多人合作容易出现混乱;2.在范围链中很容易受到干扰。3.结合上述包装函数,我们还可以通过包装函数处理“全局变量”。3.3手动取消引用变量如果业务代码中不再需要某个变量,可以手动取消引用该变量,以便可以回收。复制代码如下:var data={/*部分大数据*/};blah blahdata=null3.4善用回调除了使用闭包访问内部变量,我们还可以使用流行的回调函数进行业务处理。

复制代码如下:函数get data(回调){ var data=' some big data回调(null,数据);}getData(函数(err,data) { console.log(数据);回调函数是Continuation Passing Style(CPS)的一种技术,这种编程风格将函数的业务重心从返回值转移到回调函数。此外,它与闭包相比有很多优点:1。如果传入的参数是基本类型(如字符串、数值),回调函数传入的参数将是复制值,业务代码使用后更容易回收;2.通过回调,我们不仅可以完成同步请求,还可以在异步编程中使用它,这是现在非常流行的一种写作风格。3.回调函数本身通常是一个临时匿名函数。一旦请求的函数被执行,回调函数本身的引用将被释放和恢复。3.5良好的闭包管理当我们的业务需求(如循环事件绑定、私有属性、带参数的回调等。)必须使用闭包,请注意细节。循环事件是开始使用JavaScript闭包的必修课。让我们假设一个场景:有六个按钮对应六种事件,当用户点击按钮时,相应的事件将在指定的地方输出。复制代码如下: varb tns=document . queryselectorall('。BTN ');//6 elements var output=document . queryselector(' # output ');var事件=[1,2,3,4,5,6];//Case 1 for(var I=0;i btns.lengthi ) { btns[i]。onclick=function(evt){ output . innertext=' Clicked '事件[I];};}//Case 2 for(var I=0;i btns.lengthi ) { btns[i]。onclick=(function(index){ return function(evt){ output . innertext=' Clicked ' events[index];};})(I);}//Case 3 for(var I=0;i btns.lengthi ) { btns[i]。onclick=(function(event){ return function(evt){ output . innertext=' Clicked '事件;};})(事件[I]);}这里的第一个解决方案显然是典型的循环绑定事件错误。这里就不细说了。详情请参考我对一位网友的回复。第二种和第三种方案的区别在于闭包传递的参数。在第二种方案中,传入的参数是当前循环下标,而后者直接传入相应的事件对象。其实后者更适合大数据量的应用,因为在JavaScript函数式编程中,调用函数时传入的参数是一个基本类型对象,所以在函数体中得到的形状是一个复制的值,从而将这个值定义为函数体范围内的局部变量。事件绑定完成后,可以手动取消对事件变量的引用,以减少外部范围内的内存占用。当一个元素被删除时,相应的事件侦听器函数、事件对象和闭包函数被销毁和回收。3.6内存不是缓存。缓存在业务发展中起着重要的作用,可以减轻时间和空间资源的负担。但是,需要注意的是,内存不应该轻易用作缓存。内存是任何程序开发的宝贵资产。如果不是重要资源,请不要直接放在内存中,或者制定过期机制,自动销毁过期缓存。4.检查JavaScript的内存使用情况在正常的开发中,我们也可以使用一些工具来分析和排查JavaScript的内存使用情况。4.1 Blink/Webkit浏览器在Blink/Webkit浏览器中(Chrome、Safari、Opera等)。),我们可以借助开发者工具的Profiles工具来检查程序的内存。

4.2 Node.js中的内存检查在node . js中,我们可以使用node-heapdump和node-memwatch模块来检查内存。复制代码如下: var heap dump=require(' heap dump ');var fs=require(' fs ');var path=require(' path ');fs . WritefileSync(path . join(_ _ dirname,' app.pid '),process . PID);//.复制的代码如下: span style=' font-family : Georgia,' times new Roman ','比特流charter ',times,seriffont-size : 14px;线高: 1.5 em;'在业务代码中引入node-heapdump后,我们需要在一定的运行时间向Node.js进程发送SIGUSR2信号,这样node-heapdump就可以对堆内存进行快照。/span复制代码如下:美元kill -USR2 (cat app.pid)。这样,在文件目录中将会有一个以heapdump-sec.usec.heapsnapshot格式命名的快照文件。我们可以使用浏览器开发工具中的配置文件工具来打开并检查它。5.总结很快就到了文章的结尾。这次分享主要给大家展示了以下内容:1。JavaScript与语言层面的内存使用密切相关;2.内存管理和恢复机制。JavaScript3.如何更高效地利用内存,让生成的JavaScript能够有更多的生命力去扩展;4.遇到内存问题如何检查内存?我希望通过学习这篇文章,你能产生更好的JavaScript代码,这将让你的母亲和老板放心。

版权声明:JavaScript还谈到了内存优化是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。