当前位置: 首页 > Web前端 > HTML5

ES6每次解构赋值前都会创建一个对象吗?会不会增加GC的负担?

时间:2023-04-05 17:25:23 HTML5

本文来自知乎上的一个提问。为了程序的可读性,我们将使用ES6的解构赋值:functionf({a,b}){}f({a:1,b:2});在这个例子的函数调用中,实际上会生成一个对象?如果是这样的话,大量的函数调用会产生大量的临时对象被GC白白释放,也就是说当函数参数比较小时,还是要尽量避免解构和传递参数,并且使用传统的:functionf(a,b){}f(1,2);上面的描述实际上同时提出了几个问题:会不会生成对象?参数少的时候,是不是要尽量避免解构和传递参数?对性能(CPU/内存)的影响有多大?1、从V8字节码分析两者的性能首先,从上面给出的代码示例来看,确实会生成一个对象。但是在实际项目中,大概率是不需要生成这个临时对象的。我在使用D8之前写过一篇文章,分析javascript是如何被V8引擎优化的。因此,让我们分析您的示例代码。函数f(a,b){返回a+b;}constd=f(1,2);由于很多人没有d8,我们使用node.js代替。运行:node--print-bytecodeadd.js其中--print-bytecode可以查看V8引擎生成的字节码。在输出结果中查找[generatingbytecodeforfunction:f]:[generatingbytecodeforfunction:]Parametercount6Framesize320000003AC126862A@0:6e000002CreateClosure[0],[0],#20000003AC126862E@4:1eFBStarR010E>0000003AC1268630@6:91StackCheck98S>0000003AC1268631@7:0301LDASMI[1]0000003AC1268633@9:1Er398E>0000003AC1268639@15:51fbf9f801CallUndefinedReceiver2r0,r2,r3,[1]0000003AC126863E@20:04LdaUndefined107S>0000003AC126863F@21:951返回池(大小=Handler)16)[为函数生成字节码:f]参数计数3Framesize072E>0000003AC1268A6A@0:91StackCheck83S>0000003AC1268A6B@1:1d02Ldara191E>0000003AC1268A6D@3:2b0300Adda0,[0]94S>0000003AC1268A70@6Table:95返回常量poolHsize=)Starr0存储当前值寄存器r0中的累加器LdaSmi[1]将小整数(Smi)1加载到累加器寄存器中。而函数体只有两行代码:Ldara1和Adda0,[0]。当我们使用解构赋值后:[generatingbytecodeforfunction:]Parametercount6Framesize24000000D24A568662@0:6e000002CreateClosure[0],[0],#2000000D24A568666@4:1efbStarr010E>000000D24A568668@6:91StackCheck100S>000000D24A568669@7:6c010329f9CreateObjectLiteral[1],[3],#41,r2100E>000000D24A56866E@12:50fbf901CallUndefinedReceiver1r0,r2,[058D]620A@16:04LdaUndefined115S>000000D24A568673@17:95返回常量池(大小=2)处理程序表(大小=16)[为函数生成字节码:f]参数计数2帧大小4072E>000000D24A568AEA@0:91StackCheck000000D24A568AEB@1:1f02fbMova0,r0000000D24A568AEE@4:1dfbLdarr0000000D24A568AF0@6:8906JumpIfUNDEFIEN[6](000000D24A568AF6@12)000000D24A568AF2@8:1DFBLDARR000000000D24A568AF4@10:8810JUMPIFNOTNULL[16]000000D24A568AFA@16:0900LdaConstant[0]000000D24A568AFC@18:1ef7Starr4000000D24A568AFE@20:53e800f802CallRuntime[NewTypeError],r3-r474E>000000D24A568B03@25:93Throw74S>000000D24A568B04@26:20fb0002LdaNamedPropertyr0,[0],[2]000000D24A568B08@30:1efaStarr176S>000000D24A568B0A@32:20fb0104LdaNamedPropertyr0,[1],[4]000000D24A568B0A@32:20fb0104LdaNamedPropertyr0,[1],[4]000000D24A568B03f24A5星r285S>000000D24A568B10@38:1df9Ldarr293E>000000D24A568B12@40:2bfa06Addr1,[6]96S>000000D24A568B15@43:95ReturnConstantpool(size=2)HandlerTable(size=16)我们可以看到代码明显增加了,CreateObjectLiteral创建了一个对象原本只有2个函数,核心指令突然增加到近20条,其中有JumpIfUndefined、CallRuntime、Throw等指令。延伸阅读:理解V8字节码《翻译》2.使用--trace-gc参数查看内存由于这个内存占用很小,所以我们加了一个循环。functionf(a,b){returna+b;}for(leti=0;i<1e8;i++){constd=f(1,2);}console.log(%GetHeapUsage());%GetHeapUsage()函数有些特殊,以百分号(%)开头。这是一个用于V8引擎内部调试的函数。我们可以通过命令行参数--allow-natives-syntax来使用这些功能。node--trace-gc--allow-natives-syntaxadd.js得到了结果(为了可读性我调整了输出格式):[10192:0000000000427F50]26ms:Scavenge3.4(6.3)->3.1(7.3)MB,1.3/0.0ms分配失败[10192:0000000000427F50]34ms:清除3.6(7.3)->3.5(8.3)MB,0.8/0.0ms分配失败4424128使用解构赋值时:[7812:000000030E0045]47(6.3)->3.1(7.3)MB,1.0/0.0ms分配失败[7812:00000000004513E0]36ms:Scavenge3.6(7.3)->3.5(8.3)MB,0.7/0.0ms分配失败[7512:0000000E000]4ms:Scavenge4.6(8.3))->4.1(11.3)MB,0.5/0.0msallocationfailure4989872可以看到内存分配多了,堆空间的使用也比之前多了。使用--trace_gc_verbose参数可以查看更详细的gc信息,也可以看到这些内存都是年轻代,清理的开销比较小。3.逃逸分析通过逃逸分析,V8引擎可以去除临时对象。还要考虑前面的函数:functionadd({a,b}){returna+b;}如果我们还有一个函数double,用于将数字加倍。functiondouble(x){returnadd({a:x,b:x});}而这个double函数最终会被编译成functiondouble(x){returnx+x;}在V8引擎里面,会是逃逸分析处理的步骤如下:首先添加中间变量:functionadd(o){returno.a+o.b;}functiondouble(x){leto={a:x,b:x};returnadd(o);}对函数add的调用的内联扩展变为:functiondouble(x){leto={a:x,b:x};returno.a+o.b;}替换对字段的访问操作:functiondouble(x){leto={a:x,b:x};returnx+x;}删除未使用的内存分配:functiondouble(x){returnx+x;}通过V8的逃逸分析,将原来分配在堆上的对象移除。4.结论不要在语法层面做这种微优化,引擎会优化,业务代码更要注意可读性和可维护性。如果你是写库代码,可以试试这种优化,扩展参数,直接传递。你能带来多少性能优势取决于最终的基准测试。例如,Chrome49开始支持Proxy,直到一年后的Chrome62才对Proxy的性能进行了提升,将Proxy的整体性能提升了24%到546%。