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

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

为了程序的易读性,我们会使用 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

原文地址:https://www.cnblogs.com/lalalagq/p/9824491.html

时间: 2024-10-08 02:04:30

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

es6学习 -- 解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring). 以前,为变量赋值,只能直接指定值. let a = 1; let b = 2; let c = 3; ES6 允许写成下面这样. let [a, b, c] = [1, 2, 3]; 上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值. 本质上,这种写法属于"模式匹配",只要等号两边的模式相同,左边的变量就会被赋予对应的值.下面是一些使用嵌套数组进行解构的例子. 我认为

Es6 新增解构赋值

1.数组的解构赋值 基本用法 ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring). 要想实现解构,就必须是容器,或者具有可遍历的接口. 以前,为变量赋值,只能直接指定值. let a = 1; let b = 2; let c = 3; ES6 允许写成下面这样. let [a, b, c] = [1, 2, 3]; 上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值. 本质上,这种写法属于模式匹配:只要等号两边的模式相同,左边的变

ES6 之 解构赋值

本博文配合 阮一峰 <ES6 标准入门(第3版)>一书进行简要概述 ES6 中变量的解构赋值. 数组的解构赋值 基本用法 ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构. ES6 以前,为变量赋值,只能直接指定值. let a = 1 let b = 2 let c = 3 ES6 允许写成下面的样式. let [a, b, c] = [1, 2, 3] 该代码表示,可以从数组中提取值,按照对应位置,对变量赋值. 相关示例 本质上来说,这种写法属于“模式匹配”.即

es6变量解构赋值的用途

这里是我觉得es6解构赋值,在平时我们写js的时候非常有用,而且经常用到的地方,能简化我们的代码,让写代码简介优雅易读; 用途 1.交换变量的值,太方便了这逼,写法不仅简介而且一看就明白 let [x,y]=[1,2]; [x,y]=[y,x]; console.log(x);//输出2 console.log(y);//输出1 2.从函数返回多个值 函数只能返回一个值,如果要返回多个值,只能将它们放在数组或者对象里面返回.有了解构赋值,取出这些值那是非常的方便,最关键是易读 function

ES6语法~解构赋值、箭头函数

2015年6月17日 ECMAScript 6发布正式版本 打开VSCode终端powershell:ctrl+` 1.         定义变量:let 使用var 定义的变量没有{ }限制,在条件中定义的i,全局中都可以使用,造成变量污染,有变量提升预解析作用,只提升变量名,不提升值!降低js代码的可阅读性 相同作用域内,let不允许重复声明变量!!否则报错!!但可以更改变量值 使用let定义的变量:不会有变量提升,必须先定义后使用,否则先使用会报错: ReferenceError 在for

es6(es2015)解构赋值

?es6新增结构赋值,结构赋值分为两种 数组解构赋值,对象解构赋值. 数组解构赋值: 分别按顺序给数组中的每一项赋值.  如上所示如果c未给赋值,c为undefind. a==1,b==2 那么3并没有被保存到变量中,这时如果想要将剩余值进行保存,可以只用“...” ...c 这样的形式,可以将剩余值以数组的形式将其保存到变量c中. 另外...可用于复制数组 实际项目中有时候我们需要将函数中的实际参数放到一个数组中, es5:Array.prototype.slice.call(argument

ES6之解构赋值

何为解构赋值? 解构赋值语法是一个 Javascript 表达式,这使得可以将值从数组或属性从对象提取到不同的变量中. 如果理解起来感觉抽象,直接看下面例子: 数组解构: 我们在以前要给变量赋值需要像下面这样写: var arr=[1,2.3]; var a = arr[0]; var b = arr[1]; var c = arr[2]; 是不是感觉略繁琐了点?而如果我们用解构赋值的话,可以怎么写呢?看下面代码: let [a,b,c] = [1,2,3]; console.log(a,b,c

ES6 对象解构赋值(浅拷贝 VS 深拷贝)

对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中. 拷贝对象 let aa = { age: 18, name: 'aaa' } let bb = {...aa}; console.log(bb); // {age: 18, name: "aaa"} 合并对象 扩展运算符(...)可以用于合并两个对象 let aa = { age: 18, name: 'aaa' } let bb = { sex: '男' } let cc = {...aa, ...bb

es6的解构赋值

let {name,age}={name:'asa',age:134}; console.log(name)==>'asa' console.log(age)==>134 let [x,y]=[1,2,3] console.log(x)===>1 console.log(y)===>2