引
Chrome 都用过吧,V8 都知道吧,V8 性能杠杠的没问题吧。但是你知道吗,有这样一段神奇的代码因为 V8 的优化导致了与实际设计预期的行为发生了相悖。
Object.defineProperty
Object.defineProperty([], 'length', {value: -1, configurable: true})
Object.defineProperty([], 'len' + 'gth', {value: -1, configurable: true})
我们可以来看一下这俩段代码,从 JavaScript 的语义上来说,这俩段代码并没有什么不同,但是当我们将他们运行在 Chrome 或者 Node.js 中,我们便能得到俩个截然不同的错误。
javascript
Object.defineProperty([], 'length', {value: -1, configurable: true})
VM8179:1 Uncaught RangeError: Invalid array length
at Function.defineProperty (<anonymous>)
at <anonymous>:1:8
(anonymous) @ VM8179:1
Object.defineProperty([], 'len' + 'gth', {value: -1, configurable: true})
VM8183:2 Uncaught TypeError: Cannot redefine property: length
at Function.defineProperty (<anonymous>)
at <anonymous>:2:8
为什么呢?
那这和 V8 性能优化极好又有什么关系呢?我们可以在他们的源码中看到如下内容:
cpp
if (*name == ReadOnlyRoots(isolate).length_string()) {
// 2a. Return ArraySetLength(A, Desc).
return ArraySetLength(isolate, o, desc, should_throw);
}
简单理解一下,就是在这里直接使用了引用与系统内的常量字符串 "length"
引用进行了一个 O(1) 的快速匹配,从而优化了性能,但是也导致了我们上面的错误。
那么为什么 'length'
和 'len' + 'gth'
有区别呢?这个没有优化吗?我们可以在这《Exploring V8's strings: implementation and optimizations》看到实际上在 V8 中存在着不同的字符串,当我们使用 %DebugPrint
对俩段内容进行输出的时候,我们可以发现:
%DebugPrint('leng'+'th'):ONE_BYTE_STRING_TYPE
%DebugPrint('length'):ONE_BYTE_INTERNALIZED_STRING_TYPE
这也是为什么后者没办法作为一个常量字符串和全局只读常量字符串 "length"
进行比较的原因。
标题党!
你可能会问,你说的我都看懂了,但是和 TODO 又有什么关系呢?经常写代码都知道,我们不想处理的问题,都是先 TODO NEVER DO ,其实我们刚刚的代码上面看到一段东西:
arduino
// TODO(jkummerow): Check if we need slow string comparison.
是不是很有趣呢 V8,哈哈哈哈,希望看完这些你能对 JS 一些底层的内容产生兴趣。
后续进展
在去年年底的时候,我与 TC39 的成员 Jack Works 讨论了这个问题,最后得到了一个结论,在 ECMAScript 的设计上来看,这确实是 V8 的一个问题(虽然它是为了优化性能),但是 TC39 也没有料到 V8 的优化这么细节,居然导致了上面的问题。所以在那段时间,他们对于该行为进行了约束,并且在 TC39 的标准中加入了该检测。
当时我在 v8 bugs 中也反映了该问题,他们在最近也对这个问题进行了修复。
bugs.chromium.org/p/v8/issues...
总结
挺开心的,有一种建设世界的快乐了。