很容易想到的一个方法是通过写一个实例来比较运行时间,通过实例来看,我发现两者差距非常小,可以忽略不计。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function compare (times ) { let a = { key : {} }; let temp = a; for (let i = 0 ; i < times; i++) { let tmp = temp['key' ]; tmp['key' ] = {}; temp = tmp; } temp['key' ]['key' ] = 'surprise' ; let d0 = new Date (); let i = a; while (i['key' ] !== 'surprise' ) { i = i['key' ]; } console .log('[] time' , new Date () - d0); let d1 = new Date (); let ii = a; while (ii.key !== 'surprise' ) { ii = ii.key; } console .log('. time' , new Date () - d1); } compare(10000000 ); compare(10000001 ); compare(10000002 ); compare(10000003 );
于是我希望通过底层来比较下这两者到底有什么区别。
我想到的第一个办法是将代码转换成 AST,看看结果有什么不同。
借助于 @babel/parser
,并且打开 tokens
选项,可以看到词法分析阶段产生的 token 有什么区别。
1 2 3 4 5 6 7 8 const parser = require ('@babel/parser' );const code1 = `a.b.c.d` ;const code2 = `a['b']['c']['d']` ;const ast1 = parser.parse(code1, { tokens : true });const ast2 = parser.parse(code2, { tokens : true });console .log(ast1, ast2);
词法分析 a.b.c.d
1 2 3 4 5 6 7 8 9 10 Token: [ 0 : Token {type : TokenType, value : "a" , …} 1 : Token {type : TokenType {label : "." , …} } 2 : Token {type : TokenType, value : "b" , …} 3 : Token {type : TokenType {label : "." , …} } 4 : Token {type : TokenType, value : "c" , …} 5 : Token {type : TokenType {label : "." , …} } 6 : Token {type : TokenType, value : "d" , …} 7 : Token {type : TokenType {label : "eof" , …} } ]
a['b']['c']['d']
1 2 3 4 5 6 7 8 9 10 11 12 13 Token: [ 0 : Token {type : TokenType, value : "a" , …} 1 : Token {type : TokenType {label : "[" , …} } 2 : Token {type : TokenType, value : "b" , …} 3 : Token {type : TokenType {label : "]" , …} } 4 : Token {type : TokenType {label : "[" , …} } 5 : Token {type : TokenType, value : "c" , …} 6 : Token {type : TokenType {label : "]" , …} } 7 : Token {type : TokenType {label : "[" , …} } 8 : Token {type : TokenType, value : "d" , …} 9 : Token {type : TokenType {label : "]" , …} } 10 : Token {type : TokenType {label : "eof" , …} } ]
从词法分析生成的 token 中就可以发现区别所在,[
和 ]
可能是个变量,从这点可以猜测出 a.b.c.d
更快。
更严禁的角度:字节码 如果仅仅从词法分析阶段生成的 token 去判断,可能还不是很准备,而且这是 Babel 的 parser,并不是 V8 的 parser。
可以从字节码去比较这两者的差异,结果会更准确些。
1 2 3 4 5 6 7 8 9 function test ( ) { const a = { b : { c : { d : 1 } } }; const demo1 = a.b.c.d; const demo2 = a['b' ]['c' ]['d' ]; } test();
看下这段代码生成的字节码长什么样。
1 node --print-bytecode --print-bytecode-filter=test cast.js > bytecode.txt
生成的字节码整体非常长,借助于--print-bytecode-filter
指令可以得到想要的字节码段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [generated bytecode for function: test] Parameter count 1 Register count 4 Frame size 32 13 E> 0x1ed1ce91547e @ 0 : a5 StackCheck 30 S> 0x1ed1ce91547f @ 1 : 7d 00 00 08 CreateObjectLiteral [0], [0], #8 0x1ed1ce915483 @ 5 : 26 fb Star r0 73 S> 0x1ed1ce915485 @ 7 : 28 fb 01 01 LdaNamedProperty r0, [1], [1] 0x1ed1ce915489 @ 11 : 26 f8 Star r3 75 E> 0x1ed1ce91548b @ 13 : 28 f8 02 03 LdaNamedProperty r3, [2], [3] 0x1ed1ce91548f @ 17 : 26 f8 Star r3 77 E> 0x1ed1ce915491 @ 19 : 28 f8 03 05 LdaNamedProperty r3, [3], [5] 0x1ed1ce915495 @ 23 : 26 fa Star r1 97 S> 0x1ed1ce915497 @ 25 : 28 fb 01 01 LdaNamedProperty r0, [1], [1] 0x1ed1ce91549b @ 29 : 26 f8 Star r3 102 E> 0x1ed1ce91549d @ 31 : 28 f8 02 07 LdaNamedProperty r3, [2], [7] 0x1ed1ce9154a1 @ 35 : 26 f8 Star r3 107 E> 0x1ed1ce9154a3 @ 37 : 28 f8 03 09 LdaNamedProperty r3, [3], [9] 0x1ed1ce9154a7 @ 41 : 26 f9 Star r2 0x1ed1ce9154a9 @ 43 : 0d LdaUndefined 114 S> 0x1ed1ce9154aa @ 44 : a9 Return Constant pool (size = 4) Handler Table (size = 0)
分析字节码
You can think of V8’s bytecodes as small building blocks that make up any JavaScript functionality when composed together. V8 has several hundred bytecodes. There are bytecodes for operators like Add
or TypeOf
, or for property loads like LdaNamedProperty
. V8 also has some pretty specific bytecodes like CreateObjectLiteral
or SuspendGenerator
. The header file bytecodes.h defines the complete list of V8’s bytecodes.
可以把 V8 的字节码看作是小型的构建块,组合起来就构成了 JS 的功能。
每个字节码指定其输入和输出作为寄存器操作数。Ignition 使用寄存器 r0,r1,r2,… 和累加器寄存器。
CreateObjectLiteral 字面意思:创建对象字面量
Star r0 将值保存在某个寄存器或累加器中。
这边的意思:将这个对象字面量保存在 r0
寄存器中
LdaNamedProperty AccumulatorUse, OperandType, OperandType 可以在 V8 的头文件 中找到 LdaNamedProperty
这个字节码。
1 V(LdaNamedProperty, AccumulatorUse::kWrite, OperandType::kReg, OperandType::kIdx, OperandType::kIdx)
LdaNamedProperty r0, [1], [1]
是在内存中查找名字为 1 的值,这里表示 a.b
1 2 3 4 5 6 LdaNamedProperty r0, [1], [1] // a.b Star r3 LdaNamedProperty r3, [2], [3] // a.b.c Star r3 LdaNamedProperty r3, [3], [5] // a.b.c.d Star r1
那这段就表示 在 a 上查找 a.b.c.d
了。
1 2 3 4 5 6 LdaNamedProperty r0, [1], [1] // a['b'] Star r3 LdaNamedProperty r3, [2], [7] // a['b']['c'] Star r3 LdaNamedProperty r3, [3], [9] // a['b']['c']['d'] Star r2
同理这段是表示 a['b']['c']['d']
。
从字节码上可以看到这两种查找方式几乎没有任何区别。
简单地分析一下原因:V8 使用 HiddenClasses 来追踪对象模型;这里比较的是 JS 的上层查找方式,在 V8 中确实有不同的方式进行属性查找,这个例子中是一开始创建了对象字面量,后续并没有动态添加属性,所以两者都是使用 fast 模式进行查找。