ES6 Map 的键相等在 Chrome 内容脚本中表现异常

Key equality of ES6 Map behaves oddly in Chrome content script

在我的内容脚本中,我使用 Map 来跟踪所有打开的弹出窗口 windows。 Map中的键值对构造如下:

问题是,Map.prototype.has()Map.prototype.get() 有时 return 意外结果。

// content.js

let map = new Map();
let popup = window.open('https://www.google.com');
let data = {};

map.set(popup, data);

// retrieve data later
window.setTimeout(() => {

  // should return true, but sometimes return false
  console.log(map.has(popup));

  // should return {}, but sometimes return undefined
  console.log(map.get(popup));

}, 3000);

由于某些原因,添加的键和引用 popup 似乎并不总是被视为 "equal"。而这种模棱两可的情况似乎只存在于内容脚本中。如果上面的代码是在浏览器的控制台中执行的,那么 map.has()map.get()always return 正确的值。

所以我的问题是:为什么会这样?是不是由我不知道的某些内容脚本的底层机制引起的?

我可以重现这个(Chrome 60.0.3112.78,Windows 10,64 位)和 reported it as a bug。发现这个干得好!

这个错误在 Chrome 62.0.3175.2 (canary) 中被无意中修复了,但我使用 Wea​​kMap 构建了一个新的演示,但仍然失败 (https://jsfiddle.net/769a0qmu):

var map = new WeakMap();
var popup = window.open("https://google.com"); // requires popups to be enabled
map.set(popup, "data");
window.setTimeout(function() {
  console.log(map.get(popup)); // expect: "data"
  map.set(popup, "modified");
  console.log(map.get(popup)); // expect: "modified"
}, 3000);

在Chrome 62.0.3175.2(金丝雀)中输出是

data
data

来自错误报告的解释

Comment 10 by adamk:

The issue in V8 seems to be that globals created for a remote context return an empty string for %_ClassOf(). This causes us to go down the wrong path when looking for hash codes for such objects.

Comment 12 by adamk:

Here's what's going on inside V8. First, note from the original stack overflow report that this used to be broken in Map, but isn't anymore. Map and WeakMap used to use exactly the same logic to get the hash code for an object. That code looks like this (it's written in internal v8 JS):

function GetExistingHash(key) {
  if (IS_RECEIVER(key) && !IS_PROXY(key) && !IS_GLOBAL(key)) {
    var hash = GET_PRIVATE(key, hashCodeSymbol);
    return hash;
  }
  return %GenericHash(key);
}

Note the IS_GLOBAL(key). That's a macro which expands to %_ClassOf(key) == 'global'. Since the RemoteContext-created global object returns empty string, it fails this check and so we incorrectly try to use the normal object mechanism for dealing with hashcodes, rather than the Global-specific logic (which is handled in the C++ runtime function %GenericHash().

Over the last couple of months, v8 has moved its implementation of the Map methods out of JS, which means they're no longer sharing the above logic with WeakMap. In particular, the decision about how to find the hash code always goes directly to %GenericHash, which uses a different mechanism (an instance type check) on the key to see how to handle the hash code. This method gets the right answer for the RemoteContext case, and thus uses the special hash field on JSGlobalProxy (source), as intended.

All that said, there's the question of what to do to fix the WeakMap case. There's ongoing work inside v8 that will eventually make this go away (analogous to how Map got fixed), but that's probably on the order of weeks until it's fixed. If we'd like a fix sooner, we could try to hack things to make %_ClassOf return "global", but it's not yet clear to me how much work that'd be.

Given that this has been broken for two stable releases already, I'm inclined to just wait for it to be fixed on the v8 side. I've tentatively lowered priority and marked this as blocked on the v8 bug, but I'm open to doing something sooner if it seems worthwhile.

更新 (2017-08-24):

错误已被标记为已修复。