Quantcast RGPDソリューション「choice」とember.jsの競合

みなさん、こんにちは!
新しい情報をお届けしますし、この問題の解決策も見つけました。

さて、今日もこのバグについて掘り下げてみたところ、さらに詳しい情報が得られたと思います。

Quantcast のファイル cmp2ui-fr.js を調べてみると、バグが発生している場所がわかりました。それは以下の関数の中です(現在は minified 版しかありません):

function(t){for(var n in t){t[n].status=e;}}

ご覧の通り、この関数は for..in ループを使用しており、変数 t は配列です。以前も説明した通り、Ember.js は JavaScript のネイティブな Array を拡張しています。その拡張の一つとして、_super というエントリが追加されていることがわかりました。

この _super エントリは、Ember.js 内の _utils.ROOT を参照していると思われる ROOT() 関数を指しています(この名前がどこかで見たことがある方もいるかもしれませんが、私には馴染みがありません ^^)。そして、この ROOT() 関数は拡張不可能(non-extensible)です。

どうやらこの _super プロパティは「列挙可能(enumerable)」とみなされているらしく、配列に対して for..in ループを実行すると、_super エントリが通常の要素として扱われてしまいます(例えば、valuesbindvalueOf など、配列オブジェクトの関数たちは例外ですが)。

これは Ember.js のバグであり、望ましい動作ではないと私は考えています。

さて、このバグを非常に再現しやすい形で再現させることができました。その方法は以下の通りです。まず、オブジェクトの配列を作成します:

var objs = [{'key':'val'},{'key':'val'}];

次に、厳密モード(strict mode)で配列をループし、各要素に新しいプロパティを設定する関数を作成します:

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

この現象を見ると、このバグはすでに広く存在しており、Quantcast に固有の問題ではない可能性が高いと考えられます。基本的に、for..in ループを使用する anyone は、一貫性のない動作や重大なバグに直面するリスクがあります。

私見では、これは Discourse や Quantcast の問題というよりも、明確に Ember.js の問題です。しかし、Ember.js で修正されるまで、何らかの対処法を見つける必要があるのも事実です ^^。

良い知らせとしては、この問題を修正する方法が見つかったと思います。

一つの方法は、すべての for..in ループ(おそらく forEachfor...of などにも同様に適用)に以下の行を追加することです:
if (!objs.hasOwnProperty(i)) {continue};
これにより、配列の _super の奇妙な動作自体は修正されませんが、ローカル的にはそのアクセスを防ぐことができます。ただし、これは当然ながら、Quantcast のような制御できない外部スクリプトには適用できません。

もう一つの方法(私が推奨するアプローチ)は、JavaScript の Array プロトタイプを変更することです(つまり、Ember.js によるオーバーライドをさらにオーバーライドする ^^)。これにより、_super を非列挙可能(non-enumerable)にします。そのためには、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}); の行を追加できるかもしれません。

現時点では、私は以下の行を追加することでバグを修正しました:
//Make _super not enumerable to prevent bug between emberjs and for..in
Object.defineProperty(Array.prototype, ‘_super’, {‘enumerable’: false});

これを Quantcast の choice.js スクリプトを呼び出す前に実行しています。

ローカルでこのバグを修正したい場合は、以下の 2 行を含む fix_ember.js というファイルを作成し、リバースプロキシ(例えば Nginx)から静的に提供し、テーマのフッターにカスタマイズで <script src="/fix_emmber.js"></script> の行を追加する方法があります。スクリプトを直接記述するのではなくリンクとして追加する必要があります。これは、Discourse のスクリプト抽出機能のためです(詳細は Custom javascript in <head> disappear を参照してください)。

このトピックが他の方のお役に立てれば幸いです。明日、Ember.js にチケットを開き、これがバグなのか、あるいは非常に奇妙だが意図された動作なのかを確認します。

修正を Discourse に含める必要があるかどうか、引き続き情報をお伝えします。

PS: Extending JavaScript Natives – JavaScript, JavaScript… の Angus Croll 氏に心から感謝します。彼の投稿が非常に役立ちました!