著作一覧 |
Rails5で標準装備(特に設定とかしなくても勝手にトランスパイラが動いて処理できる)だったので、時代遅れ感はわかっていたがCoffeeScriptを使っていたのだが、とんでもない地雷を埋め込んでそれが爆発したので記録。
CoffeeScriptは書式が多少Rubyっぽいので、ときどき終端endを書いてしまうことがある。
if foo console.log('foo!') end
実際はCoffeeScriptはPythonのようなインデントベースなので上記のコードのendは不要だし、そもそも予約語ですらないのでエラーとなる。
ここで重要なのはendは予約語ではないという点で、そのためstrictモードで動作させるCoffeeScriptではReferenceError
がスローされるということだ。
というわけで通常は上記のようなコードを書くと例外を食らうのですぐに気づくのだが、以下のようなコードを記述していたため、まったく気づかずに半年以上稼働させていた。
ready = -> displayStartEndPeriod = -> start = $('#foo-start-date').val() end = $('#foo-end-date').val() # 予約語ではないので変数名として利用できる $('#date-disaply-div').text(formatBlaBla(start, end)) ... fooBar = (foo) -> if someCondition(foo) console.log('foo!') end displayStartEndPeriod() fooBar($('#baz').val($('#baz').text())) $(document).ready(ready)
このコードに対して、期間表示をサーバー側処理へ移動したので、displayStartEndPeriod関数を削除した。その結果、特定条件で(上のコードだとfooBar関数のパラメータfooがsomeConditionを満たす場合)例外が発生するようになった。
もちろん例外がスローされた場所はわかるので見た瞬間にfooBarのendが悪いということはわかる。
わからないのは、なぜこれが半年も稼働していたかだ。
mergeのタイミングか? とか調べても半年間上記のfooBarで動いていたことは間違いない。
でしばらく考えてやっとわかった。
CoffeeScriptが生成するJavaScriptでは、すべての変数はトップレベルの関数内に定義される。
したがって上の例からは以下のようなJavaScriptが生成される。
(function() { var ready; # トップレベル関数用の変数 ready = function() { var start; var end; var displayStartEndPeriod; var fooBar; displayStartEndPeriod = function() { start = $('#foo-start-date').val(); end = $('#foo-end-date').val(); $('#date-disaply-div').text(formatBlaBla(start, end)); } ... fooBar = function(foo) { if (someCondition(foo)) { console.log('foo!') } retun end; } displayStartEndPeriod(); fooBar($('#baz').val($('#baz').text())); } $(document).ready(ready); }).call(this);
displayStartEndPeriod関数でend変数を利用しているのに対して、ready関数のスコープでend変数を宣言しているため、fooBar関数内でのendという記述が既存変数に対する参照となるために動作していたのだった。
この状態でdispalyStartEndPeriod関数を削除すれば、start,end両変数の宣言も削除される。しかしfooBar関数の中身はそのままとなる(endに対して代入が行われていないので宣言は作られない)。結果としてundefinedなendに対する参照が行われてReferenceErrorがスローされる。
なぜ、end付きifで動作していたのか実に不思議だった。
ジェズイットを見習え |