レガシーなサイトのHTMLを触っていると、<a href="javascript:void(0)">という謎の呪文に出くわすことがありますよね。ビルド後のミニファイされたJSを覗いても、やたらとvoid 0が並んでいる。なんとなく「何もしないおまじない」だと思ってスルーしがちですが、中身を知ると意外と奥が深いんです。今回はvoid 0の正体とundefinedとの違い、そして今どきのフロントエンドでjavascript:void(0)をどう置き換えるべきかを、MDNや公式ドキュメントを参照しながら整理してみます。
結論:void 0 は「どんな式を評価してもundefinedを返す」演算子
まず結論から。voidは関数ではなく、typeofやdeleteと同じ「単項演算子」です。後ろに置いた式をいったん評価して、その結果を捨て、必ずundefinedを返します。つまりvoid 0は「0を評価して、捨てて、undefinedを返す」だけの式なんですね。0である必要はまったくなく、void 1でもvoid "8192"でも結果は同じundefinedになります。
const god = 8192;
console.log(void 0); // undefined
console.log(void god); // undefined
console.log(void "GOD"); // undefined
慣習的に0が使われているのは、単純に一番短くて副作用のない値だからでしょう。ECMAScriptの仕様書を見ても、void演算子の処理は「オペランドを評価して値を取り出し、undefinedを返す」というだけのシンプルなもの。評価はちゃんと行われるので、void console.log("hi")のように書けばログは出ます。「何も実行しない」のではなく「実行はするけど戻り値を捨てる」、ここが最初のポイントです。
void(0) の括弧は関数呼び出しではない
よく見かけるvoid(0)という書き方、関数呼び出しっぽく見えますが違います。この括弧はただのグループ化演算子で、void (0)と書いているのと同じ。MDNでも「void(0)」と「void 0」は等価だと明記されています。個人的には関数と誤解されやすいのでvoid 0派ですが、チームのコーディング規約に合わせればOKです。
ただし、括弧をまったく意識しないと痛い目を見ることがあります。voidは優先順位の高い単項演算子なので、void 2 === "2"は(void 2) === "2"と解釈されてfalseになります。比較結果をまるごと捨てたいなら、void (2 === "2")と括弧で囲む必要があるわけです。こういうのはレビューで見落としがちなので、迷ったら括弧、が安全ですね。
void 2 === "2"; // (void 2) === "2" → false
void (2 === "2"); // undefined
なぜ undefined と書かずに void 0 を使うのか
「undefinedが欲しいなら素直にundefinedと書けばいいじゃん」と思いますよね。僕も最初はそう思いました。実際、今の環境ではほとんどの場合それで問題ありません。それでもvoid 0が使われ続けてきたのには、歴史的な事情と実務的な事情の2つがあります。
ひとつは、undefinedが実は予約語ではないという、JavaScriptらしいクセに由来するもの。もうひとつは、ビルドツールがファイルサイズを削るために使っているというもの。どちらも知っておくと、ミニファイ後のコードを読むときやレガシーコードを改修するときに「ああ、これね」と落ち着いて対処できるようになります。
undefinedは上書き・シャドウできてしまう
ESLintのno-voidルールの解説によると、ES5より前の時代はグローバルのundefinedが書き換え可能でした。つまりundefined = 1と誰かが書いた瞬間、x === undefinedという比較が信用できなくなる。そこで、どんな状況でも確実に本物のundefinedを返すvoid 0が重宝されていたわけです。
現在のブラウザでは、グローバルのundefinedは書込不可・設定不可のプロパティになっています。ただしMDNにもある通り、undefinedは予約語ではないので、関数の中などグローバル以外のスコープでは変数名として使えてしまいます。const undefined = "foo"みたいなコードを書く人はまずいないと思いますが(いたら全力で止めます)、理屈の上では今でも「void 0のほうが確実」なんですね。
// こんなことはしないこと!(やめてぇーー!)
(() => {
const undefined = "foo";
console.log(undefined, typeof undefined); // foo string
console.log(void 0); // undefined
})();
ミニファイで文字数が減る
もうひとつの理由はもっと現実的で、単純に短いから。undefinedは9文字、void 0は6文字です。たった3文字ですが、大規模なバンドルでは何百・何千箇所と出てくるので、積み重なると無視できません。ESLintのドキュメントでも、void 0がundefinedより短いことからミニファイ用途で使われる、と紹介されています。
なので、ビルド後のJSをデベロッパーツールのソースパネルで開いたときにvoid 0がズラッと並んでいても、それはバグでも暗号でもなく「undefinedを圧縮した姿」です。ソースマップを当てれば、元のコードでは普通にundefinedと書かれているはず。僕はこれを知るまで、minifyされたファイルを見るたびに「何この呪文……」と固まっていました。
「javascript:void(0)」の意味と、今はもう使わないほうがいい理由
さて本題。検索でここに来た方の多くは、おそらく<a href="javascript:void(0)">の意味が知りたかったんじゃないでしょうか。これは「javascript:」というURLスキームとvoid演算子の合わせ技です。javascript:で始まるURLにブラウザが遷移しようとすると、URLの中身がスクリプトとして実行されます。
やっかいなのは、そのスクリプトの結果が文字列だった場合、ブラウザはそれをHTMLとして扱い、ページの中身を置き換えてしまうこと。そこで最後にvoid(0)を置いて結果を強制的にundefinedにし、「実行だけして、画面は何も変えない」リンクを作っていたわけです。jQuery全盛期のサイトやCMSの古いテンプレートでは、今でも普通に現役で見かけますね。
<!-- うん、昔はよく書いてました -->
<a href="javascript:void(0)" onclick="openModal()">詳しく見る</a>
セキュリティ面:任意のコードが動く入口になる
MDNのjavascript: URLのページには、この仕組みはeval()と同じように任意のコード実行につながりうるため、使用は推奨されないという警告がはっきり書かれています。自分で書いたvoid(0)自体は無害でも、「hrefにjavascript:を書ける」設計そのものが攻撃面になるということですね。
Reactチームも2019年のv16.9のリリースで、javascript:で始まるURLを危険な攻撃面だとして非推奨にしました。ユーザーのプロフィールURLのような未検証の値をhrefに流し込んだ瞬間、スクリプトが仕込まれる可能性があるからです。当時の発表では「16.9では警告、将来のメジャーバージョンではエラーにする」と予告されていて、eslint-plugin-reactにjsx-no-script-urlという専用ルールまで用意されています。
アクセシビリティ面:リンクなのにリンクじゃない
もうひとつの問題が、見た目はリンクなのに中身はボタンという「なんちゃってボタン」になってしまうこと。MDNの<a>要素のページでは、hrefに#やjavascript:void(0)を入れてクリックイベントで処理する使い方を、偽物のボタンとして乱用されるパターンだと指摘しています。
具体的には、新しいタブで開こうとしたり、リンクをコピーしたり、ブックマークしたりしたときに意図しない動きになります。スクリーンリーダーにも「リンク」として読み上げられるので、押したら画面遷移すると期待したユーザーを裏切ることに。業務で使うツールやECサイトほど、こういう地味な違和感がストレスになるんですよね。
javascript:void(0) の代わりに書くべきコード
じゃあ何に置き換えればいいのか。判断基準はシンプルで、「どこか別の場所に移動するならリンク、その場で何かを実行するならボタン」です。モーダルを開く、アコーディオンを開閉する、タブを切り替える……こういった処理はほぼすべてボタンの仕事です。
見た目をリンク風にしたいからaタグを使う、という理由でjavascript:void(0)を書いていたケースも多いと思いますが、見た目はCSSでどうとでもなります。ここからは実際に置き換えるときのパターンを2つ紹介します。レガシー改修のときにそのままコピペして使ってください。
処理を実行するだけなら <button type=”button”>
モーダルを開くなど「その場で完結する処理」は、素直にbutton要素にしましょう。type=”button”を付けておかないと、form要素の中に置いたときに送信ボタン扱いになってしまうので要注意です。キーボード操作(EnterやSpace)やフォーカスの扱いもブラウザが面倒を見てくれるので、aタグで自前実装するより圧倒的にラクです。
見た目をテキストリンクに寄せたいときは、ボタンのデフォルトスタイルをリセットするだけ。背景と枠線を消して、色と下線をリンクっぽくすればほぼ見分けがつきません。フォーカスリングまで消してしまうとキーボードユーザーが迷子になるので、:focus-visibleは残しておくのがおすすめです。
<!-- 良さげなサンプル -->
<button type="button" class="text-link" id="openModal">詳しく見る</button>
<style>
.text-link {
background: none;
border: 0;
padding: 0;
font: inherit;
color: inherit;
text-decoration: underline;
cursor: pointer;
}
</style>
<script>
document.getElementById('openModal').addEventListener('click', () => {
openModal();
});
</script>
遷移先があるなら本物のURL + preventDefault
一方で「JavaScriptが動けばその場でモーダル表示、動かなければ詳細ページに遷移」のように、ちゃんと行き先がある場合はaタグのままでOKです。hrefには実在するURLを書き、クリック時にevent.preventDefault()でデフォルトの遷移を止めてからJS側の処理を走らせます。
この書き方なら、新しいタブで開きたい人は普通に開けるし、JSの読み込みに失敗しても最低限ページ遷移はできる。ちなみにhref=”#”で代用する書き方もよく見ますが、ページのトップにスクロールしたりURLに#が付いたりするので、javascript:void(0)の代わりとしては微妙です。(現場で一番よく見るパターンなんですけどね……)
<a href="/items/8192/" class="js-quick-view">商品詳細を見る</a>
<script>
document.querySelectorAll('.js-quick-view').forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
openQuickView(link.href);
});
});
</script>
void演算子が今でも活躍する場面
ここまで読むと「voidってもう要らない子なのでは?」と思うかもしれません。でも、javascript:void(0)がダメなだけで、void演算子そのものは今でも便利な場面があります。むしろモダンなJavaScriptやTypeScriptのほうが出番がある、というのが個人的な印象です。
キーワードは「戻り値を捨てることを明示する」。値を返す式をあえて無視したいとき、voidを付けておくと読み手にもLinterにも意図が伝わります。代表的な2つのパターンを見てみましょう。
アロー関数の戻り値を意図せず返さない
アロー関数は中括弧を省略すると、式の結果をそのまま返します。MDNでは、checkbox.onclick = () => doSomething();と書いたとき、doSomething()がfalseを返すとハンドラーの戻り値もfalseになり、チェックボックスのデフォルト動作が止まってしまう例が紹介されています。
これを防ぐのが() => void doSomething()という書き方。戻り値が必ずundefinedになるので、関数側の仕様が変わってもイベントハンドラーの挙動が勝手に変わりません。地味ですが、他人が書いた関数やライブラリの関数を呼ぶときの保険としてはかなり有効です。
// doSomething() が false を返すとチェックできなくなる……
checkbox.onclick = () => doSomething();
// 戻り値を捨てるので安心
checkbox.onclick = () => void doSomething();
Promiseを「あえて待たない」ことを明示する
TypeScriptでtypescript-eslintを使っていると、awaitもcatchもしていないPromiseをno-floating-promisesルールで怒られることがありますよね。このルールはデフォルトで、void演算子を付けたPromiseを「意図的に待たないもの」として扱ってくれます。ログ送信のような待つ必要のない非同期処理は、void sendLog()と書けばOKです。
ただしESLint本体のno-voidルールを有効にしていると、今度はvoidそのものが禁止されて板挟みになります。その場合はno-voidのallowAsStatementオプションをtrueにして、文としてのvoidだけ許可するのが定番の落としどころです。ちなみにTypeScriptの型注釈で使う: voidは「戻り値を使わない」という型の話で、void演算子とは別物なので混同しないようにしましょう。
// eslint.config.js(抽粋)
rules: {
'no-void': ['error', { allowAsStatement: true }],
'@typescript-eslint/no-floating-promises': 'error',
}
// 待たなくていい非同期処理
void sendLog('clicked');
まとめ
void 0は「式を評価して、必ずundefinedを返す」だけの単項演算子です。void(0)の括弧は関数呼び出しではなくただのグループ化で、ミニファイ後のコードにvoid 0が大量にあるのは、undefinedより短く、かつ確実にundefinedを得られるからでした。正体が分かれば、もう呪文には見えないはずです。
そしてjavascript:void(0)は、javascript: URLで画面を書き換えないためのテクニックとして生まれたもの。ただ、セキュリティとアクセシビリティの両面から今は非推奨で、Reactでも警告の対象です。その場で処理するならbutton type=”button”、遷移先があるなら本物のURL+preventDefault、と覚えておけば迷いません。
一方で、アロー関数の戻り値を捨てたり、Promiseをあえて待たないことを明示したりと、void演算子そのものは今でも現役です。覚え方は「voidが出てきたら、戻り値を捨てたいサイン」。「Void」は英語で「空っぽ」という意味なので、結果を空っぽにする演算子と記憶しておけば、たぶんそんなにズレてないと思います。
レガシーコードの改修でjavascript:void(0)を見つけたら、ぜひこの記事を思い出して置き換えてみてください。JavaScriptの記法まわりでは、「JavaScriptの文字列をスッキリ複数行で書く方法【テンプレートリテラル】」や「ソースコード内における「?(はてなマーク)」の意味と使い方【JavaScript】」の記事も書いているので、あわせてどうぞ。
【参考文献】
- void 演算子 – JavaScript | MDN(https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/void)
- undefined – JavaScript | MDN(https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/undefined)
- javascript: URLs – URIs | MDN(https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/javascript)
- <a>: アンカー要素 – HTML | MDN(https://developer.mozilla.org/ja/docs/Web/HTML/Element/a)
- ECMAScript® Language Specification — The void Operator(https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-void-operator)
- React v16.9.0 and the Roadmap Update – React Blog(https://ja.legacy.reactjs.org/blog/2019/08/08/react-v16.9.0.html)
- no-void – ESLint(https://eslint.org/docs/rules/no-void)
- no-floating-promises | typescript-eslint(https://typescript-eslint.io/rules/no-floating-promises)