大家好,
我有一些新信息要告诉大家,并且已经找到了这个问题的解决方案。
今天我又深入研究了这个 bug,发现了一些更多信息。
通过查看 quantcast 的 cmp2ui-fr.js 文件,我找到了 bug 发生的位置,它在这个函数中(我们只有压缩版本):
function(t){for(var n in t){t[n].status=e;}}
如你所见,这个函数使用了 for..in 循环,而变量 t 是一个数组。我们之前已经解释过,ember.js 扩展了 JavaScript 的原生 Array 对象。其中一个修改就是添加了一个 _super 属性。
这个 _super 属性指向某个 ROOT() 函数,该函数似乎引用了 ember.js 中的 _utils.ROOT(也许这对你们中的一些人很熟悉,但对我来说并不熟悉 ^^)。这个 ROOT() 函数是不可扩展的。
显然,这个 _super 属性被认为是可枚举的,这意味着在对数组使用 for..in 循环时,_super 会被当作一个“普通”条目(不像 values、bind、valueOf 等数组对象中的函数)。
我认为这是 ember.js 的一个 bug,而不是期望的行为。
因此,我已经能够让这个 bug 非常可复现。具体做法是:创建一个对象数组,如下所示:
var objs = [{'key':'val'},{'key':'val'}];
然后,创建一个使用严格模式的函数,遍历数组并为每个条目设置一个新属性:
var tst_func = function (objs){'use strict';for(var i in objs){objs[i].newproperty = true }};
最后,调用这个函数处理对象数组:
tst_func(objs);
你会收到一个错误:Uncaught TypeError: can't define property "newproperty": Function is not extensible。
看到这一点让我觉得这个 bug 很可能已经广泛存在,并且并不特定于 quantcast。基本上,任何使用 for..in 的人都可能遇到不一致的行为或严重 bug。
对我来说,这并不完全是 Discourse 或 quantcast 的问题,而显然是 ember.js 的问题。不过,在 ember.js 修复之前,我们仍然需要找到解决方法 ^^。
好消息是,我认为我已经找到了修复方法。
一种方法是在每个 for..in 循环(可能也包括 for each、for of 等)中添加一行:if (!objs.hasOwnProperty(i)) {continue};。这不会改变 Array 的 _super 异常行为,但在本地阻止访问它。显然,这意味着这种方法不适用于我们无法控制的外部脚本,比如我的 quantcast 用例。
另一种方法,我认为这是正确的方向,是修改 JavaScript 的 Array 原型(也就是覆盖 ember.js 自己的覆盖 ^^),使 _super 不可枚举。为此,我们需要在 ember.js 被调用和评估之后运行以下 JavaScript 代码:
//Make _super not enumerable to prevent bug between emberjs and for..in
Object.defineProperty(Array.prototype, '_super', {'enumerable': false});
第二种方法仍然允许直接访问 _super(正如预期),同时防止它在循环中出现。不过,我无法保证这种异常行为不会被任何 ember.js 内部函数或外部插件使用。
查看文件 _ember_jquery-189e46ebcb33594b835e782fd1ce916ec750bc0cf980ebc4fb7796649161a18d.js,我看到了一些特定于 Discourse 的行,例如:
var ALIASES = {
"ember-addons/ember-computed-decorators":
"discourse-common/utils/decorators",
"discourse/lib/raw-templates": "discourse-common/lib/raw-templates",
"preload-store": "discourse/lib/preload-store",
"fixtures/user_fixtures": "discourse/tests/fixtures/user-fixtures",
};
var ALIAS_PREPEND = {
fixtures: "discourse/tests/",
helpers: "discourse/tests/",
};
我们是否可以在这里添加我们的 Object.defineProperty(Array.prototype, '_super', {'enumerable': false}); 行?
目前,我通过以下方式本地修复了这个 bug:在调用 quantcast 的 choice.js 脚本之前,添加以下两行:
//Make _super not enumerable to prevent bug between emberjs and for..in
Object.defineProperty(Array.prototype, '_super', {'enumerable': false});
我认为有人可以通过创建一个名为 ‘fix_ember.js’ 的文件(包含这两行),并通过反向代理(例如 Nginx)静态提供它,然后在主题页脚中使用自定义功能添加 <script src="/fix_emmber.js"></script> 行来本地修复这个 bug。由于 Discourse 的脚本提取机制(参见 https://meta.discourse.org/t/custom-javascript-in-head-disappear/144356),直接写入脚本而不是创建链接是行不通的。
我希望这个主题能帮助到其他人。我明天将在 ember.js 上提交一个工单,看看这到底是一个 bug 还是一种非常奇怪但有意为之的行为。
我会随时向大家汇报,看看我们是否需要在 Discourse 中包含修复方案。
PS:非常感谢 Angus Croll 在 Extending JavaScript Natives – JavaScript, JavaScript… 上的文章,他的帖子对我帮助很大!