2025年10月10日金曜日

Automatic Semicolon Insertion; ASIで沼った話

Automatic Semicolon Insertion; ASIで沼った話

久々に原因特定に時間が掛かったので備忘録。
今回問題になったのが「Automatic Semicolon Insertion」、通称ASIによる影響です。
直訳すれば「自動セミコロン挿入」ですが、簡単に言うと「セミコロン(;)で処理を自動的に終了位置として判定してくれるよ」という仕組み。
コード自体が勝手に書き換わるわけではなく、JavaScriptエンジンがセミコロンを「挿入したつもり」で文の境界を解釈します。
便利な機能ではあるのですがちょっとした「落とし穴」になりえるのでJavaScript弄っている人は暇つぶしにでも読んでみてください。

ASIって何?(=処理末端解釈)

ASIの最大の目的は、開発者の利便性向上です。JavaScriptはインタープリタ系言語の代表格で、型指定が不要、変数の自動型変換が可能という「ゆるさ」が売り。
数千行のコードを書くときに、毎文の終わりにセミコロンを手動で付けるなんて面倒くさいですよね? ASIがあれば、エンジンが自動でセミコロンを「仮想挿入」して文を区切ってくれるので、セミコロン漏れの心配が減ります。

JavaScriptのようなスクリプト言語は、C++やJavaのようなコンパイル言語と比べて、型の指定が不要であったり、変数の型が自動変換されたりと、開発の自由度やスピードが重視される傾向にあります。ASIもまた、開発者の負担を減らし、柔軟な記述を可能にする自然な流れと言えます。

ASIの落とし穴

ASIの問題は、文の境界が曖昧になる点にあります。
簡単に言えば「自動判定」が誤作動する…いやまぁ機械的に処理してるわけだから正確にはユーザーがASIが意識の外にある場合があるってだけですが。
最も有名なのがreturn文の例です。

return // ここで改行
a + 1
本来は a + 1 の結果を返したい意図で書いたとしても、JavaScriptは改行された時点で「ここで処理が終わったな」と判断し、内部的に以下のように解釈します。
return; //  ASIによってセミコロンが挿入
a + 1;  //  別の処理として解釈

結果、returnは何も値を返さず処理が終了し、期待した動作になりません。JavaScriptは変数名やメソッド名の途中ではない限り、基本的にどこでも改行可能なので、この自由さがASIと衝突することで、予期せぬ挙動を生み出すのです。

今回、ASIで沼った話

さて、本題の「ASIで沼った話」。これは単純なイベントリスナー登録の話です。
と、その前に、通常はこの様に書きますが
document.getElementById(id).addEventListener(type, listener);
個人コーディングの時はBSCオリジナルラッパーを使っているので
$i('aaa').ael('change', bbb);
こんな感じに圧縮記述出来ます。

普段、可読性を重視して、リスナーは次のように列挙して記述しています。
$i('aaa1').ael('change', bbb)
$i('aaa2').ael('change', bbb)
$i('aaa3').ael('change', bbb)
これは何の問題もありません。ASIによって、各行の末尾にセミコロンが補完され、それぞれ独立した処理として実行されます。まさに「ASI様様」という状態です。

リスナーをまとめてみた

今回は同じ関数を呼ぶ処理があったので可読性と効率を考えて、forEachでまとめて登録することにしました。
// リスナーまとめエリアでの記述
['aaaAbc','aaaDef','aaaGhi'].forEach(id=>{$i(id).ael('change', aaaAAA);});
このコード自体はテストして問題もありません。
DOMも読み込み済みでこれといったエラーはありません。

ところが、このコードを「リスナーまとめエリア」に移動した瞬間、aaaGhiの要素でエラーが発生! 原因不明で頭を抱えました。DOMはOK、個別記述ならOK、隔離してもOK、上部移動してもOK。でも、このエリアに置くとダメ…。

沼の原因:配列の誤解釈

forEachをばらせば正常になるのでバラで登録しても良かったんですが今後の為にも一応調査した事で一応特定は出来ました。
今回の具体的な配置は以下の通りです。
$i('aaa1').ael('change', bbb) // 1行目
$i('aaa2').ael('change', bbb) // 2行目
$i('aaa3').ael('change', bbb) // 3行目
['aaaAbc','aaaDef','aaaGhi'].forEach(id=>{$i(id).ael('change', aaaAAA);}); // 4行目
実は、内部的に発生していたのは、3行目と4行目の結合でした。
$i('aaa3').ael('change', bbb)['aaaAbc','aaaDef','aaaGhi'].forEach(id=>{$i(id).ael('change', aaaAAA);});
JavaScriptのインタプリタはこの様に解釈しました。

具体的には:
  1. $i('aaa3').ael('change', bbb) の実行結果(addEventListenerは通常 undefined などの値を返します)
  2. その結果に対して、次の行の配列をインデックスとしてアクセスしようとする
  3. undefinedなどの値に配列アクセス([ ])を試みたためエラーが発生
  4. 結果的に「undefined.addEventListener」のような状態になり、処理全体が破綻した

改行によって処理の継続と判断される箇所で、たまたま次の行が(や[など、前の行の処理の続きと解釈できる記号で始まっていたため、ASIが発動せず、予期せぬ文法解釈(配列アクセス)が行われてしまったという事ですね。

利便性 vs リスク:JavaScriptのジレンマ

結局、ASIでセミコロンから解放されたはずなのに、「セミコロンを明示的に付けた方が安全」
という話になりますが、そうなると「ASIの存在意義は?」という根本的な問いに行き着きます

①自由さと厳密さのトレードオフ
JavaScriptの設計思想は元々「柔軟な記述」を重視していますが、現在のコーディングの潮流はより厳密な方向に向かっています。

②TypeScriptという名のJavaScript
TypeScriptという言葉を聞いたことがある人も居るかもしれませんが、JavaScriptに静的型付けの概念を持ち込み、コンパイル時に厳密なチェックを可能することでコーディングを厳格堅牢にできます。
ですが・・・結局のTypeScriptはJavaScriptに過ぎません。コンパイルという表現を使いますが一般的なプログラムのように機械語にするわけでは無い為、処理が高速化するわけでもありません。
JavaScriptは結局ブラウザ依存です。つまり、折角型を付けたコードを削除して低級のJavaScriptに戻すというここだけ聞くと馬鹿らしい処理をしています。

個人的に最近はモジュール形式(.mjs)で書くことが増えましたが、これはローカルスコープやstrict modeの適用など、より厳格なコーディングを促しますがASIは有効です。

JavaScriptの未来:柔軟性と厳格性の狭間

正直、現状のJavaScriptがどこを目指しているのか、掴みどころがありません。
柔軟性を保ち続けたいのか、それともTypeScriptのような厳格な方向へ完全にシフトしてしまうのか。言語には通常、何らかの明確な「信念」がありますよね。
例えば、C言語系は高速実行を、Javaはプラットフォーム非依存を、Pythonは読みやすさと生産性を、PerlやRubyは簡潔さを、それぞれの強みとして進化させてきました。一方、JavaScriptは「どっちつかず」の状態が長すぎて、信念のなさが逆にその独自の魅力——いや、ブレブレの個性——を生んでいる気がします。
もちろん、見守るしかない部分もありますが、心から願うのは、この「ゆるさ」が失われないことくらいでしょうか・・・。


まとめ
  • セミコロンを積極的に付ける:ASIに頼りすぎず、明示的に。
  • 改行前に一文完結を確認:returnやメソッドチェーンの後、特に注意。
  • ツール活用:PrettierやESLintで自動整形・警告を。
  • テストを忘れず:個別動作OKでも、文脈で壊れる可能性あり。