本文来源于知乎上的一个提问。

为了程序的易读性,我们会使用 ES6 的解构赋值:

function f({a,b}){}
f({a:1,b:2});

这个例子的函数调用中,会真的产生一个对象吗?如果会,那大量的函数调用会白白生成很多有待 GC 释放的临时对象,那么就意味着在函数参数少时,还是需要尽量避免采用解构传参,而使用传统的:

function f(a,b){}
f(1,2);

上面的描述其实同时提了好几个问题:

  1. 会不会产生一个对象?
  2. 参数少时,是否需要尽量避免采用解构传参?
  3. 对性能(CPU/内存)的影响多大?

1. 从 V8 字节码分析两者的性能表现

首先从上面给的代码例子中,确实会产生一个对象。但是在实际项目中,有很大的概率是不需要产生这个临时对象的。

我之前写过一篇文章 使用 D8 分析 javascript 如何被 V8 引擎优化的。那么我们就分析一下你的示例代码。

function f(a,b){return a+b;
}const d = f(1, 2);

鉴于很多人没有 d8,因此我们使用 node.js 代替。运行:

node --print-bytecode add.js

其中的 --print-bytecode 可以查看 V8 引擎生成的字节码。在输出结果中查找 [generating bytecode for function: f]

[generating bytecode for function: ]
Parameter count 6
Frame size 320000003AC126862A @    0 : 6e 00 00 02       CreateClosure [0], [0], #20000003AC126862E @    4 : 1e fb             Star r010 E> 0000003AC1268630 @    6 : 91                StackCheck 98 S> 0000003AC1268631 @    7 : 03 01             LdaSmi [1]0000003AC1268633 @    9 : 1e f9             Star r20000003AC1268635 @   11 : 03 02             LdaSmi [2]0000003AC1268637 @   13 : 1e f8             Star r398 E> 0000003AC1268639 @   15 : 51 fb f9 f8 01    CallUndefinedReceiver2 r0, r2, r3, [1]0000003AC126863E @   20 : 04                LdaUndefined 107 S> 0000003AC126863F @   21 : 95                Return
Constant pool (size = 1)
Handler Table (size = 16)
[generating bytecode for function: f]
Parameter count 3
Frame size 072 E> 0000003AC1268A6A @    0 : 91                StackCheck 83 S> 0000003AC1268A6B @    1 : 1d 02             Ldar a191 E> 0000003AC1268A6D @    3 : 2b 03 00          Add a0, [0]94 S> 0000003AC1268A70 @    6 : 95                Return
Constant pool (size = 0)
Handler Table (size = 16)

Star r0 将当前在累加器中的值存储在寄存器 r0 中。

LdaSmi [1] 将小整数(Smi)1 加载到累加器寄存器中。

而函数体只有两行代码:Ldar a1 和 Add a0, [0]

当我们使用解构赋值后:

[generating bytecode for function: ]
Parameter count 6
Frame size 24000000D24A568662 @    0 : 6e 00 00 02       CreateClosure [0], [0], #2000000D24A568666 @    4 : 1e fb             Star r010 E> 000000D24A568668 @    6 : 91                StackCheck 100 S> 000000D24A568669 @    7 : 6c 01 03 29 f9    CreateObjectLiteral [1], [3], #41, r2100 E> 000000D24A56866E @   12 : 50 fb f9 01       CallUndefinedReceiver1 r0, r2, [1]000000D24A568672 @   16 : 04                LdaUndefined 115 S> 000000D24A568673 @   17 : 95                Return
Constant pool (size = 2)
Handler Table (size = 16)
[generating bytecode for function: f]
Parameter count 2
Frame size 4072 E> 000000D24A568AEA @    0 : 91                StackCheck 000000D24A568AEB @    1 : 1f 02 fb          Mov a0, r0000000D24A568AEE @    4 : 1d fb             Ldar r0000000D24A568AF0 @    6 : 89 06             JumpIfUndefined [6] (000000D24A568AF6 @ 12)000000D24A568AF2 @    8 : 1d fb             Ldar r0000000D24A568AF4 @   10 : 88 10             JumpIfNotNull [16] (000000D24A568B04 @ 26)000000D24A568AF6 @   12 : 03 3f             LdaSmi [63]000000D24A568AF8 @   14 : 1e f8             Star r3000000D24A568AFA @   16 : 09 00             LdaConstant [0]000000D24A568AFC @   18 : 1e f7             Star r4000000D24A568AFE @   20 : 53 e8 00 f8 02    CallRuntime [NewTypeError], r3-r474 E> 000000D24A568B03 @   25 : 93                Throw 74 S> 000000D24A568B04 @   26 : 20 fb 00 02       LdaNamedProperty r0, [0], [2]000000D24A568B08 @   30 : 1e fa             Star r176 S> 000000D24A568B0A @   32 : 20 fb 01 04       LdaNamedProperty r0, [1], [4]000000D24A568B0E @   36 : 1e f9             Star r285 S> 000000D24A568B10 @   38 : 1d f9             Ldar r293 E> 000000D24A568B12 @   40 : 2b fa 06          Add r1, [6]96 S> 000000D24A568B15 @   43 : 95                Return
Constant pool (size = 2)
Handler Table (size = 16)

我们可以看到,代码明显增加了很多,CreateObjectLiteral 创建了一个对象。本来只有 2 条核心指令的函数突然增加到了近 20 条。其中不乏有 JumpIfUndefinedCallRuntimeThrow 这种指令。

  • 扩展阅读:理解 V8 的字节码「译」

2. 使用 --trace-gc 参数查看内存

由于这个内存占用很小,因此我们加一个循环。

function f(a, b){return a + b;
}for (let i = 0; i < 1e8; i++) {const d = f(1, 2);
}console.log(%GetHeapUsage());

%GetHeapUsage() 函数有些特殊,以百分号(%)开头,这个是 V8 引擎内部调试使用的函数,我们可以通过命令行参数 --allow-natives-syntax 来使用这些函数。

node --trace-gc --allow-natives-syntax add.js

得到结果(为了便于阅读,我调整了输出格式):

[10192:0000000000427F50]
26 ms: Scavenge 3.4 (6.3) -> 3.1 (7.3) MB, 1.3 / 0.0 ms  allocation failure[10192:0000000000427F50]
34 ms: Scavenge 3.6 (7.3) -> 3.5 (8.3) MB, 0.8 / 0.0 ms  allocation failure4424128

当使用解构赋值后:

[7812:00000000004513E0]
27 ms: Scavenge 3.4 (6.3) -> 3.1 (7.3) MB, 1.0 / 0.0 ms  allocation failure[7812:00000000004513E0]
36 ms: Scavenge 3.6 (7.3) -> 3.5 (8.3) MB, 0.7 / 0.0 ms  allocation failure[7812:00000000004513E0]
56 ms: Scavenge 4.6 (8.3) -> 4.1 (11.3) MB, 0.5 / 0.0 ms  allocation failure4989872

可以看到多了因此内存分配,而且堆空间的使用也比之前多了。使用 --trace_gc_verbose 参数可以查看 gc 更详细的信息,还可以看到这些内存都是新生代,清理起来的开销还是比较小的。

3. Escape Analysis 逃逸分析

通过逃逸分析,V8 引擎可以把临时对象去除。

还考虑之前的函数:

function add({a, b}){return a + b;
}

而这个 double 函数最终会被编译为

function double(x){return x + x;
}

在 V8 引擎内部,会按照如下步骤进行逃逸分析处理:

首先,增加中间变量:

function add(o){return o.a + o.b;
}function double(x) {let o = {a:x, b:x};return add(o);
}

把对函数 add 的调用进行内联展开,变成:

function double(x) {let o = {a:x, b:x};return o.a + o.b;
}

替换对字段的访问操作:

function double(x) {let o = {a:x, b:x};return x + x;
}

删除没有使用到的内存分配:

function double(x) {return x + x;
}

通过 V8 的逃逸分析,把本来分配到堆上的对象去除了。

4. 结论

不要做这种语法层面的微优化,引擎会去优化的,业务代码还是更加关注可读性和可维护性。如果你写的是库代码,可以尝试这种优化,把参数展开后直接传递,到底能带来多少性能收益还得看最终的基准测试。

举个例子就是 Chrome 49 开始支持 Proxy,直到一年之后的 Chrome 62 才改进了 Proxy 的性能,使 Proxy 的整体性能提升了 24% ~ 546%。

原文发布时间为:2018年06月28日
原文作者:justjavac
本文来源:掘金      如需转载请与原作者联系

ES6 的解构赋值前每次都创建一个对象吗?会加重 GC 的负担吗?相关推荐

  1. ES6常用解构赋值有哪几种?

    ES6常用解构赋值有哪几种? a.数组的解构赋值 //数组解析赋值,模式匹配 {let [a, b, c] = [1, 2, 3];console.log("模式匹配");cons ...

  2. ES6语法~解构赋值、箭头函数、class类继承及属性方法、map、set、symbol、rest、new.target、 Object.entries......

    2015年6月17日 ECMAScript 6发布正式版本 前面介绍基本语法,  后面为class用法及属性方法.set.symbol.rest等语法. 一.基本语法:  1.         定义变 ...

  3. es6学习 -- 解构赋值

    ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring). 以前,为变量赋值,只能直接指定值. let a = 1; let b = 2; let c ...

  4. ES6中解构赋值深入解读

    ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构 1.数组的解构赋值 let [head, ...tail] = [1, 2, 3, 4]; head // 1 tail ...

  5. ES6语法---解构赋值

    解构赋值概念 按照一定的模式,从数组和对象中提取值,对变量进行赋值,就被称为解构. 目的是为了提高效率,使用起来更加方便. 以下的各个说明,我会类比着ES5去解释,希望能帮到小伙伴们. 数组解构 正常 ...

  6. 前端面试不用怕!一分钟带你了解es6的解构赋值

    解构赋值(★★★)!!!!! ES6中允许从数组中提取值,按照对应位置,对变量赋值,对象也可以实现解构 <script>var stus=['李钟硕','刘诗诗','易烊千玺']//访问数 ...

  7. ES6:解构赋值及对象方法

    解构赋值 对象解构 起别名:通过:来进行取名 //对象解构赋值 var obj = { uname: "张三", age: 21, sex: "男" } // ...

  8. 让java支持es6_简单看看es6解构赋值

    哎,我真的是太难了,今天就被这个解构赋值(也可以叫做析构,貌似析构是在c++中的,所以我这里叫做解构赋值吧)弄的我很烦,本来以为很容易的,结果还是弄了好久...就总结一下解构吧! 1.解构的基本使用 ...

  9. 【ES6】变量的解构赋值

    [ES6]变量的解构赋值 一.什么叫解构赋值? 二.解构赋值有哪些分类?写法? 1)对数组的解构赋值 2)对对象的解构赋值 3)对字符串的解构赋值 4)对数值和布尔值的解构赋值 5)对函数参数的解构赋 ...

最新文章

  1. 身为 Java 程序员必须掌握的 10 款开源工具!
  2. 非系统表空间损坏,rman备份恢复
  3. LUA 运行期间不独占线程的递归,通过回调实现
  4. python函数与函数式编程
  5. Java常见排序算法之直接选择排序
  6. 讲一下Asp.net core MVC2.1 里面的 ApiControllerAttribute
  7. 【FFMPEG源码终极解析】 av_packet_alloc 与 av_packet_free
  8. web前端入门学习(纯干货)
  9. 让电脑说话代码_让您的代码为您说话
  10. Oracle 项目就是那回事 ----表空间的管理
  11. Visual Studio BI 中维度的KeyColumns属性
  12. java ztree_ztree简介_动力节点Java学院整理
  13. 突发!Log4j 爆“核弹级”漏洞,Flink、Kafka等至少十多个项目受影响,微博、京东、网易等大厂都发起应急响应...
  14. 微信 - 微信语音转发好友 / 朋友圈方法
  15. html如何用表格做二级菜单栏,Excel表格制作二级下拉菜单步骤
  16. python数据拟合方法_Python-最小二乘法曲线拟合【转载】
  17. 开源项目的版权声明已无存在必要?
  18. 讲清楚之 javascript原形
  19. 常用存储器(SRAM、DRAM、NVRAM、PSRAM)简单介绍
  20. 《Presto(Trino)——The Definitive Guide》CHAPTER 6 Connectors Advanced CHAPTER 7 Connector Examples

热门文章

  1. java vo转map_javabean实体类对象转为Map类型对象的方法(转发)
  2. php redis list llen,redis llen list 命令简介
  3. python redis pipeline 堆积_Redis Pipeline python
  4. python可以用eclipse开发吗_Eclipse不是主要用来写Java么?Python也可以在eclipse上面写?Eclipse搭建Python开发环境...
  5. oracle导入dmp文件数据,dmp文件导入Oracle数据库
  6. 分片上传,断点续传,还有秒传
  7. matlab保存数据save,[转载]matlab中save,load使用方法
  8. python合并文件夹下的文件_Python实现合并同一个文件夹下所有PDF文件的方法示例...
  9. 读《学习JavaScript数据结构与算法》 第二章
  10. Android内容提供者(群发短信)