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

为了程序的易读性,我们会使用 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 32
0000003AC126862A @ 0 : 6e 00 00 02 CreateClosure [0], [0], #2
0000003AC126862E @ 4 : 1e fb Star r0
10 E> 0000003AC1268630 @ 6 : 91 StackCheck
98 S> 0000003AC1268631 @ 7 : 03 01 LdaSmi [1]
0000003AC1268633 @ 9 : 1e f9 Star r2
0000003AC1268635 @ 11 : 03 02 LdaSmi [2]
0000003AC1268637 @ 13 : 1e f8 Star r3
98 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 0
72 E> 0000003AC1268A6A @ 0 : 91 StackCheck
83 S> 0000003AC1268A6B @ 1 : 1d 02 Ldar a1
91 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 24
000000D24A568662 @ 0 : 6e 00 00 02 CreateClosure [0], [0], #2
000000D24A568666 @ 4 : 1e fb Star r0
10 E> 000000D24A568668 @ 6 : 91 StackCheck
100 S> 000000D24A568669 @ 7 : 6c 01 03 29 f9 CreateObjectLiteral [1], [3], #41, r2
100 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 40
72 E> 000000D24A568AEA @ 0 : 91 StackCheck
000000D24A568AEB @ 1 : 1f 02 fb Mov a0, r0
000000D24A568AEE @ 4 : 1d fb Ldar r0
000000D24A568AF0 @ 6 : 89 06 JumpIfUndefined [6] (000000D24A568AF6 @ 12)
000000D24A568AF2 @ 8 : 1d fb Ldar r0
000000D24A568AF4 @ 10 : 88 10 JumpIfNotNull [16] (000000D24A568B04 @ 26)
000000D24A568AF6 @ 12 : 03 3f LdaSmi [63]
000000D24A568AF8 @ 14 : 1e f8 Star r3
000000D24A568AFA @ 16 : 09 00 LdaConstant [0]
000000D24A568AFC @ 18 : 1e f7 Star r4
000000D24A568AFE @ 20 : 53 e8 00 f8 02 CallRuntime [NewTypeError], r3-r4
74 E> 000000D24A568B03 @ 25 : 93 Throw
74 S> 000000D24A568B04 @ 26 : 20 fb 00 02 LdaNamedProperty r0, [0], [2]
000000D24A568B08 @ 30 : 1e fa Star r1
76 S> 000000D24A568B0A @ 32 : 20 fb 01 04 LdaNamedProperty r0, [1], [4]
000000D24A568B0E @ 36 : 1e f9 Star r2
85 S> 000000D24A568B10 @ 38 : 1d f9 Ldar r2
93 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 这种指令。

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 failure 4424128

当使用解构赋值后:

[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 failure 4989872

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

3. Escape Analysis 逃逸分析

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

还考虑之前的函数:

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

如果我们还有一个函数,double,用于给一个数字加倍。

function double(x) {
return add({a:x, b:x});
}

而这个 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%。

原文地址:https://www.zhihu.com/question/282228797/answer/427739238

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

  1. es6的解构赋值学习(1)

    相对es5的简单的"="赋值来说,es6增加了一种新的赋值模式--解构赋值,按照它的规则,可以从数组和对象中提取值来对变量进行赋值,个人觉得方便了很多,但是这个模式有点恶心人,相比 ...

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

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

  3. Es6 新增解构赋值

    1.数组的解构赋值 基本用法 ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring). 要想实现解构,就必须是容器,或者具有可遍历的接口. 以前,为 ...

  4. ES6 之 解构赋值

    本博文配合 阮一峰 <ES6 标准入门(第3版)>一书进行简要概述 ES6 中变量的解构赋值. 数组的解构赋值 基本用法 ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这 ...

  5. ES6变量解构赋值

    ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构ES6之前我们申明多个变量需要按照下面的方法: let l a=1; let b=2; let c=3; let d=4; ...

  6. ES6变量解构赋值的用法

    一.数组赋值(从数组中提取值,按照对应位置,对变量赋值) 1. 完全解构(变量与值数目相等) let arr = [1, 2, 3]; let [a,b,c] = arr; console.log(a ...

  7. es6 -- 与解构赋值默认值结合使用

    参数默认值可以与解构赋值的默认值,结合起来使用. function foo({x, y = 5}) { console.log(x, y) } foo({}) // undefined 5 foo({ ...

  8. ES6之解构赋值

    截止到ES6,共有6种声明变量的方法,分别是var .function以及新增的let.const.import和class: 我们通常的赋值方法是: var foo='foo'; function ...

  9. 进军es6(2)---解构赋值

    本该两周之前就该总结的,但最近一直在忙校招实习的事,耽误了很久.目前依然在等待阿里HR面后的结果中...但愿好事多磨!在阿里的某轮面试中面试官问到了es6的掌握情况,说明es6真的是大势所趋,我们更需 ...

随机推荐

  1. SpringCloud+MyBatis+Redis整合—— 超详细实例(一)

    1.SpringCloud+MyBatis MyBatis 是一款优秀的轻量级半自动持久层框架,与之相对应的还有hibernate框架.①   话不多说,接下来搭建SpringCloud+MyBati ...

  2. windows7安装完上不了网

    安装完Windows7后发现上不了网,网卡驱动没有安装的原因:但是没有网怎么下载驱动呢,,先装一个网卡版驱动精灵(貌似叫驱动精灵万能网卡版)什么的安装一个网卡驱动,就解决了

  3. Windows进程通信之一看就懂的匿名管道通信

    目录 进程通信之一看就懂的匿名管道通信 一丶匿名管道 1.1何为匿名管道 1.2创建匿名管道需要注意的事项 1.3 创建匿名管道需要的步骤 1.4代码例子 1.5代码运行截图 进程通信之一看就懂的匿名 ...

  4. Kettle-Data Integration 简介

    Pentaho系列产品介绍   Pentaho公司下面有一堆关于数据处理(数据整合.数据挖掘.报表等)的开源项目即社区版,入口网站: http://community.pentaho.com/ 数据整 ...

  5. linux笔记学习大全,包括相关软件

    1.如果tomcat正常启动,内部浏览器可以访问,外部电脑的浏览器布恩那个访问? 可以使用如下命令: iptables -F firewall-cmd --add-port=8080/tcp --pe ...

  6. 【转】c# winform 创建文件,把值写入文件,读取文件里的值,修改文件的值,对文件的创建,写入,修改

    创建文件和读取文件的值 #region 判断文件是否存在,不存在则创建,否则读取值显示到窗体 public FormMain() { InitializeComponent(); //ReadFile ...

  7. SpringMVC04 很杂很重要(注解,乱码处理,通配符,域属性调用,校正参数名称,访问路径,请求、响应携带参数,请求方法)

    1.导入架包 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3 ...

  8. 私有npm下载资源

    私有npm库下载资源需要用户名和密码,这个需要创建npm库的人提供. 使用方法: npm login --registry=仓库地址 Username: 用户名 Password: 密码 Email: ...

  9. 报错:'byte' does not name a type

    这个错误是因为你在.cpp/.h中使用 byte 这个类型,把他修改成int就ok了

  10. LeetCode Valid Palindrome 有效回文(字符串)

    class Solution { public: bool isPalindrome(string s) { if(s=="") return true; ) return tru ...