$('.js-menu').on('click', ...) はスラスラ書けるのに、案件で「jQuery使わないでほしい」と言われた瞬間、手が止まる——。
オンラインスクールや学習教材でjQueryから入った方なら、似た経験があるのではないでしょうか。私自身もそうでした。$(...) が手に染みついていて、バニラJSに置き換えると「this が消えた」「NodeListが map できない」と小さなつまずきが積み重なります。
この記事では、以下の3つを持ち帰れるように書いています。
- jQuery ↔ バニラJSの対応表で、脳内マップを作れる
- 11パターン分のコード例をコピペして試せる
- 今でもjQueryを使っていい場面がわかる(=使い分けの判断軸が手に入る)
私自身、案件によっては今もjQueryを書きます。大事なのは道具を状況で選べることです。この記事も、どちらかを貶める内容ではありません。
なお、11パターンすべてを順に読む必要はありません。次の対応表から、いま手元で詰まっているパターンを拾い読みしてOKです。
- そもそも、なぜ今バニラJSに書き換えるのか——「jQueryは悪」ではない
- パターン別・書き換え対応
- パターン1 — 読み込みタイミング(
$(function() { ... })→DOMContentLoaded) - パターン2 — 要素の取得(
$('.foo')→querySelector/querySelectorAll) - パターン3 — イベントの登録(
.on('click')→addEventListener) - パターン4 — クラス操作(
addClass→classList) - パターン5 — 属性・data属性・プロパティ(
attr/prop/data) - パターン6 — DOM生成・削除(
append/html/text/remove) - パターン7 — Ajax通信(
$.ajax/$.get/$.post→fetch) - パターン8 — slideToggle相当(CSS transition +
classListまたは<details>) - パターン9 — アニメーション(
.animate()→ CSS transition / Web Animations API) - パターン10 — フォーム値の取得(
.val()→value/FormData) - パターン11 — 繰り返し処理(
$.each→forEach/for...of)
- パターン1 — 読み込みタイミング(
- いつjQueryを使い続けていいか——「脱jQuery」は目的じゃない
- 書き換え時のハマりどころベスト3
- まとめ——対応表を手元に、1ファイルずつ置き換えてみよう
- 関連記事
そもそも、なぜ今バニラJSに書き換えるのか——「jQueryは悪」ではない
最初にはっきりさせておきたいのは、jQueryは悪ではないということです。2010年代のフロントエンドを支えた偉大なライブラリで、今もWordPressコアが読み込み続けているくらい現役です。「jQueryから入った自分は遅れている」と感じる必要はありません。
ただ、ここ数年で以下のような変化が積み重なってきました。
- モダンブラウザの標準API(
querySelector・classList・fetch)が十分に成熟した - 表示速度・LCP対策で、jQueryライブラリ本体の読み込みを外したいケースが増えた
- React・Vue・Svelteなどのフレームワーク、WordPressのブロックエディタ周りでは、jQueryに依存しない実装が求められる
- 求人要件で「素のJavaScriptでDOM操作できる方」が明示されるようになってきた
こうした流れの中で、バニラJSを書ける必要性は着実に上がっています。
私自身、最近の案件では基本的にjQueryを使わず、なるべくバニラJSで書くようにしています。「使わない」と決めて書き始めると、標準APIだけでも意外と困らない場面が多い、というのが実感です。
だからといって、jQueryを捨てる必要はありません。私がおすすめしたいのは、バニラJSを「もう一つの引き出し」として持つことです。
- 新規LPや軽量なコーポレートサイトでは、バニラJSで書く
- 既存のWordPressテーマ・プラグインに手を入れるときは、そのままjQueryで書く
こうした使い分けの第一歩として、11個の書き換えパターンを整理しました。
パターン別・書き換え対応
まずは全体像から。下の表が、この記事のマップです。
| 項目 | カテゴリ | jQuery | バニラJS(対応) | 難易度 |
|---|---|---|---|---|
| パターン1 | 読み込みタイミング | $(function() { ... }) | document.addEventListener('DOMContentLoaded', ...) | 易 |
| パターン2 | 要素取得 | $('.foo') / $('#bar') | querySelector / querySelectorAll | 易 |
| パターン3 | イベント登録 | $('.btn').on('click', fn) | element.addEventListener('click', fn) | 易 |
| パターン4 | クラス操作 | addClass / removeClass / toggleClass / hasClass | classList.add / remove / toggle / contains | 易 |
| パターン5 | 属性操作 | attr() / prop() / data() | getAttribute / setAttribute / dataset | 中 |
| パターン6 | DOM生成・削除 | append / html / text / remove | insertAdjacentHTML / appendChild / createElement / innerHTML / textContent / .remove() | 中 |
| パターン7 | Ajax通信 | $.ajax / $.get / $.post | fetch | 中 |
| パターン8 | slideToggle相当 | slideToggle() / slideUp() / slideDown() | CSS transition + classList.toggle(または <details>) | 中〜難 |
| パターン9 | アニメーション | animate({ opacity: 0 }, 300) | CSS transition or Web Animations API | 中 |
| パターン10 | フォーム値取得 | $('#name').val() | element.value / element.checked / FormData | 易〜中 |
| パターン11 | 繰り返し処理 | $.each(array, fn) / $('li').each(fn) | forEach / for...of / querySelectorAll().forEach | 易 |
上から登場頻度順に並べています。手元のコードで使っている順に置き換えていけばOKです。
難易度は私の体感です。1〜4は直感的に置き換えられ、5〜7は「ハマりどころ」あり、8〜9は「そもそも設計を変える」判断が入ることもあります。
パターン1 — 読み込みタイミング($(function() { ... }) → DOMContentLoaded)
jQueryでおなじみの「DOM準備完了待ち」の処理です。
// jQuery — 2種類の書き方は同じ意味
$(document).ready(function () {
console.log('DOM ready');
});
$(function () {
console.log('DOM ready');
});バニラJSでは DOMContentLoaded イベントを使います。
// バニラJS
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM ready');
});実は、書かなくていい場合もある
読み込みタイミング処理は、スクリプトの置き場所次第で不要になります。
<script>を</body>の直前に置く → DOM生成後に実行される<script defer src="...">で読み込む → HTMLパース後に実行される
私は保守性の観点で defer を好んで使います。「いつ実行されるか」が属性で明示されるので、後から読む人が迷いません。
ハマりポイント — すでに読み込み完了後にリスナーを登録するケース
動的にスクリプトを差し込む場合など、DOMが既に準備できているタイミングで DOMContentLoaded を登録するとイベントが発火しません。readyState で分岐するのが安全です。
// 「もう準備できているなら即実行、そうでないなら待つ」の安全パターン
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
function init() {
console.log('DOM ready');
}パターン2 — 要素の取得($('.foo') → querySelector / querySelectorAll)
いちばん使う操作です。jQueryの $() は単一・複数どちらもカバーしましたが、バニラJSでは呼び分けます。
基本の対応
// jQuery
const $el = $('#main');
const $items = $('.item');// バニラJS
const el = document.getElementById('main');
// または
const el2 = document.querySelector('#main');
const items = document.querySelectorAll('.item');単一取得なら getElementById が最速ですが、セレクタで統一したいなら querySelector で問題ありません。.・# のセレクタ記法がそのまま使えるので、jQueryからの移行はスムーズです。
ハマりポイント1 — NodeListは配列ではない
querySelectorAll が返すのは配列ではなく NodeList です。forEach は使えますが、map / filter はスプレッド構文か Array.from で配列化してから使います。
// バニラJS
const items = document.querySelectorAll('.item');
items.forEach((item) => {
// forEachは使える
});
// map / filter を使いたいときは配列化
const texts = [...items].map((item) => item.textContent);
const visible = Array.from(items).filter((item) => !item.hidden);ハマりポイント2 — 見つからないと null が返る
jQueryの $('.存在しない') は「長さ0のjQueryオブジェクト」を返すので、後続処理でもエラーになりにくいです。一方、バニラJSの querySelector は要素が見つからないと null を返します。そのままメソッドを呼ぶとエラーです。
// 見つからないケースを想定して防御的に書く
const modal = document.querySelector('.js-modal');
modal?.classList.add('is-open');失敗例デモ — null を無視して呼ぶ
// セレクタにタイプミスがあったり、まだ要素が存在しない場合
const el = document.querySelector('.js-moda');
el.classList.add('is-open');
// Uncaught TypeError: Cannot read properties of null (reading 'classList')オプショナルチェーン(?.)を添える癖を付けておくと事故が減ります。エラーで止まる分、jQueryより原因に気づきやすい面もあります。
パターン3 — イベントの登録(.on('click') → addEventListener)
基本の対応
// jQuery
$('.btn').on('click', function (e) {
console.log('clicked');
});// バニラJS
const btn = document.querySelector('.btn');
btn.addEventListener('click', (e) => {
console.log('clicked');
});e.preventDefault() や e.stopPropagation() はjQueryでもバニラでも同じなので、そこは安心してください。
イベント委譲はバニラだとどう書くか
jQueryで頻出のイベント委譲(動的生成の要素にまとめてイベントを付ける書き方)は、学習者が一番詰まるところです。
// jQuery(委譲)
$('.list').on('click', '.item', function (e) {
console.log(this.textContent);
});バニラJSでは、親要素にリスナーを付けて closest で対象を絞るのが定番です。
// バニラJS(委譲・event.currentTarget 推奨)
const list = document.querySelector('.list');
list.addEventListener('click', (e) => {
// e.target は実際にクリックされた要素(.item の子孫かもしれない)
// e.currentTarget は .list(addEventListener を付けた要素)
const item = e.target.closest('.item');
// .list の外側に同名クラスがある場合を防ぐガード
if (!item || !list.contains(item)) return;
console.log(item.textContent);
});event.currentTarget を第一推奨にしたい理由
jQueryではイベントハンドラ内の this が「リスナーを付けた要素」を指しました。バニラJSではアロー関数で書くと this が変わってしまいます。そこでおすすめしたいのが event.currentTarget です。
e.target= 実際にイベントが発生した要素(子孫要素のことがある)e.currentTarget=addEventListenerを付けた要素(=jQueryのthisと同じ)
アロー関数でも event.currentTarget は問題なく取れるので、「迷ったら event.currentTarget」を覚えておくと書き方がブレません。
失敗例デモ — アロー関数で this を使う
// NG: アロー関数内の this はボタン要素を指さない(モジュールでは undefined、非モジュールでは window)
document.querySelector('.btn').addEventListener('click', (e) => {
console.log(this.textContent); // undefined
});
// OK: e.currentTarget を使う
document.querySelector('.btn').addEventListener('click', (e) => {
console.log(e.currentTarget.textContent);
});
// 参考: function記法なら this === e.currentTarget
document.querySelector('.btn').addEventListener('click', function (e) {
console.log(this.textContent);
});jQueryの癖で this を書くと、undefinedのまま進む「無言の失敗」になり気づきにくいです。event.currentTarget に寄せておくと、関数記法が変わっても書き方がブレません。
パターン4 — クラス操作(addClass → classList)
実装で頻繁に使う操作です。対応を覚えれば、機械的に置き換えられます。
対応表
| jQuery | バニラJS |
|---|---|
addClass('is-open') | classList.add('is-open') |
removeClass('is-open') | classList.remove('is-open') |
toggleClass('is-open') | classList.toggle('is-open') |
hasClass('is-open') | classList.contains('is-open') |
// jQuery
$('.menu').addClass('is-open');
$('.menu').removeClass('is-open');
$('.menu').toggleClass('is-open');
if ($('.menu').hasClass('is-open')) { /* ... */ }// バニラJS
const menu = document.querySelector('.menu');
menu.classList.add('is-open');
menu.classList.remove('is-open');
menu.classList.toggle('is-open');
if (menu.classList.contains('is-open')) { /* ... */ }複数クラスの扱い方が違う
jQueryはスペース区切りの文字列で複数クラスを指定できましたが、バニラJSは可変長引数で渡します。
// jQuery — スペース区切り
$('.menu').addClass('is-open is-visible');
// バニラJS — カンマで引数を並べる
menu.classList.add('is-open', 'is-visible');状態クラスの命名はFLOCSS流が便利
.is-open .is-active のような状態クラスは、FLOCSSのStateパターンで書くと、CSSとJSの役割がクリアになります。CSS設計の全体像は FLOCSSとは?基本の考え方と実際の書き方を分かりやすく解説 で紹介しています。
パターン5 — 属性・data属性・プロパティ(attr / prop / data)
混乱しやすいパターンです。「HTML属性」と「DOMプロパティ」の違いが絡むため、jQueryの感覚でそのまま書くとハマります。
基本の対応
| jQuery | バニラJS |
|---|---|
attr('href') / attr('href', '...') | getAttribute('href') / setAttribute('href', '...') |
prop('checked') | element.checked |
data('id') | element.dataset.id |
// jQuery
const href = $('.link').attr('href');
$('.link').attr('href', '/new');
const checked = $('.agree').prop('checked');
const id = $('.btn').data('userId'); // data-user-id属性// バニラJS
const link = document.querySelector('.link');
const href = link.getAttribute('href');
link.setAttribute('href', '/new');
const agree = document.querySelector('.agree');
const checked = agree.checked;
const btn = document.querySelector('.btn');
const id = btn.dataset.userId; // data-user-id → dataset.userIddata-user-id のようにハイフンを含む属性は、dataset.userId とキャメルケースに変換されます(jQueryの .data('userId') と似た感覚)。
ハマりポイント — dataset は常に文字列(型の違い)
- jQuery
.data()は「型推論」をします。数値っぽい文字列は数値に、JSON文字列はオブジェクトに、自動変換されます - バニラJS
datasetは常に文字列を返します
<button class="js-btn" data-user-id="42" data-role="admin">送信</button>// jQuery — 型推論する
const id1 = $('.js-btn').data('userId'); // 42(数値として返る)
// バニラJS — 常に文字列
const btn = document.querySelector('.js-btn');
const id2 = btn.dataset.userId; // "42"(文字列)失敗例デモ — 「永遠にfalse」になるパターン
// NG: 文字列 "42" と数値 42 を比較しているので一致しない
if (btn.dataset.id === 42) {
console.log('一致'); // ここは通らない
}
// OK: 数値に変換して比較
if (Number(btn.dataset.id) === 42) {
console.log('一致');
}
// OK: 文字列で比較
if (btn.dataset.id === '42') {
console.log('一致');
}jQueryからの書き換え時にもっとも無言で失敗するパターンです。if文が通らない理由がわからず時間を溶かしがちなので、要注意ポイントとして覚えておきたいところです。
パターン6 — DOM生成・削除(append / html / text / remove)
jQueryが便利だった領域です。バニラJSでは用途別に使い分け、同時にXSS(クロスサイトスクリプティング)リスクを意識します。
対応と使い分け
| 用途 | バニラJSでの書き方 | 安全性 |
|---|---|---|
| 文字列をそのまま入れたい | textContent | 最も安全 |
| 固定のHTML文字列を挿入 | innerHTML / insertAdjacentHTML | 注意(後述) |
| 要素を組み立てて挿入 | createElement + appendChild | 安全 |
| 削除 | .remove() | 安全 |
// jQuery
$('.title').text('こんにちは');
$('.list').append('<li class="item">新規</li>');
$('.item').remove();// バニラJS — textContent(安全・推奨)
document.querySelector('.title').textContent = 'こんにちは';
// バニラJS — insertAdjacentHTML(自作の固定文字列のみ)
document.querySelector('.list').insertAdjacentHTML(
'beforeend',
'<li class="item">新規</li>'
);
// バニラJS — createElement + appendChild(要素を組み立てる)
const li = document.createElement('li');
li.className = 'item';
li.textContent = '新規';
document.querySelector('.list').appendChild(li);
// バニラJS — 削除
document.querySelector('.item').remove();appendChild は要素オブジェクト、insertAdjacentHTML はHTML文字列を渡します。目的に応じて選んでください。
XSSリスク — ユーザー入力は textContent に寄せる
innerHTML / insertAdjacentHTML にユーザー入力や外部データをそのまま入れるのは避けてください。<img src=x onerror="..."> のような属性イベント経由で、ブラウザ上のスクリプト実行を許してしまう恐れがあります。
失敗例デモ — XSS発火パターン
// NG: ユーザー入力を innerHTML に入れるとスクリプトが動いてしまう
const userInput = '<img src=x onerror="alert(1)">';
document.querySelector('.comment').innerHTML = userInput;
// ページを開いた人の画面で alert が発火する
// OK: ユーザー入力は textContent で表示する
document.querySelector('.comment').textContent = userInput;
// 画面には <img src=x onerror="alert(1)"> という文字列がそのまま表示される(スクリプトは発火しない)使い分けは「ユーザー入力・外部データ → textContent」「自分で書いた固定HTML → insertAdjacentHTML or innerHTML」「要素を組み立てる → createElement + appendChild」の3択で覚えてしまうと迷いません。どうしてもHTMLを含むユーザー入力を扱う必要がある場合は、DOMPurifyなどのサニタイズライブラリの利用を検討してください。
パターン7 — Ajax通信($.ajax / $.get / $.post → fetch)
jQueryの $.ajax を fetch に置き換えるパターンです。WordPress案件では送信形式を2種類使い分けるため、ここは少し長めに見ていきます。
基本の書き換え(async / await)
// jQuery
$.ajax({
url: '/api/users',
method: 'POST',
data: JSON.stringify({ name: 'taro' }),
contentType: 'application/json',
success: (res) => console.log(res),
error: (err) => console.error(err),
});// バニラJS(async / await)
async function createUser() {
try {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'taro' }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}
}
createUser();$.ajax のオプションは fetch の第2引数に移ります。成功・失敗は try / catch で包むと読みやすいです。
ハマりポイント — fetch はHTTPエラーでrejectしない
$.ajax では404や500が返ると error コールバックに入りました。しかし fetch は404でも500でも、通信が成立していれば成功扱いになります。if (!res.ok) throw のひと手間を癖にしておくと安全です。
失敗例デモ — res.ok をチェックせず json() を呼ぶ
// NG: 404でも catch に入らない
const res = await fetch('/api/users/999');
const data = await res.json(); // 404のエラーページJSONを読んでしまう
// OK: res.ok で明示的にチェック
const res2 = await fetch('/api/users/999');
if (!res2.ok) throw new Error(`HTTP ${res2.status}`);
const data2 = await res2.json();WordPress Ajax は2種類ある
WordPress案件では、宛先によって送信形式が変わります。混同すると 400 / 403 になりやすいポイントです。
- WP REST API(
/wp-json/wp/v2/...)→ JSON形式で送る admin-ajax.php(従来の管理画面Ajax)→ フォーム形式で送る
WordPress REST API 版(JSON + X-WP-Nonce)
// WordPress REST API(例: 投稿を取得)
async function fetchPosts() {
const res = await fetch('/wp-json/wp/v2/posts', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// wp_localize_script で wpApiSettings をフロントに渡しておく
'X-WP-Nonce': wpApiSettings.nonce,
},
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}認証が必要なエンドポイントでは X-WP-Nonce ヘッダーが必須です。wp_localize_script でフロントにnonceを渡しておきましょう。
admin-ajax.php 版(URLSearchParams)
// admin-ajax.php(従来のフォーム形式)
async function loadMore() {
const body = new URLSearchParams({
action: 'load_more_posts', // 必須: PHP側で wp_ajax_ / wp_ajax_nopriv_ に紐づく
page: '2',
nonce: myAjax.nonce, // wp_localize_script で渡す
});
// Content-Type は URLSearchParams が自動で設定する
const res = await fetch(myAjax.ajaxurl, {
method: 'POST',
body,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}admin-ajax.php にJSONを送ると、PHP側で受け取れず失敗します。action パラメータを含むフォーム形式で送ると覚えておきましょう(URLSearchParams を使えば Content-Type は自動設定されます)。
パターン8 — slideToggle相当(CSS transition + classList または <details>)
jQueryの slideToggle は1行で高さ展開・折りたたみが動く便利機能でした。バニラJSでは少し工夫が要りますが、代替手段は大きく3つです。
- CSS transition +
classList.toggle(推奨) <details>/<summary>タグ(セマンティクス重視)- Web Animations API(細かく制御したいとき)
1. CSS transition + classList.toggle
もっとも汎用的なパターンです。定番は max-height のtransition。
<button class="js-toggle" type="button" aria-expanded="false" aria-controls="panel">
開閉
</button>
<div id="panel" class="panel">
<p>折りたたまれるコンテンツ</p>
</div>.panel {
overflow: hidden;
max-height: 0;
transition: max-height 0.3s ease;
}
.panel.is-open {
max-height: 500px; /* 中身より大きい値。中身が超えると見切れるので、可変コンテンツなら grid-template-rows トリックを検討 */
}const btn = document.querySelector('.js-toggle');
const panel = document.getElementById('panel');
btn.addEventListener('click', () => {
const isOpen = panel.classList.toggle('is-open');
btn.setAttribute('aria-expanded', String(isOpen));
});aria-expanded と aria-controls を添えておくと、スクリーンリーダー利用者にも開閉状態が伝わります。aria-controls の値は対象要素の id と一致させるのがルール(上の例では id="panel" と aria-controls="panel")。
もうひとつの書き方 — grid-template-rows: 0fr → 1fr トリック
2024年頃から広まった新しい書き方として、grid-template-rows を 0fr ⇄ 1fr でtransitionする方法もあります。高さが可変なコンテンツに強いのが利点ですが、比較的新しい手法なので案件要件次第では採用判断が分かれます。古めの環境では max-height のほうが無難です。
2. <details> / <summary> タグ
アコーディオンやFAQのようなUIなら、HTML標準の <details> タグが一番ラクです。
<details class="faq">
<summary>よくある質問です</summary>
<p>回答の本文です。</p>
</details>
最大の利点は、キーボード操作とスクリーンリーダー対応が自動で効くこと。JSなしでアクセシブルなアコーディオンが作れるので、要件に合うなら第一選択肢にしても良いと思います。
ハンバーガーメニューへの応用
classList.toggle と aria-expanded の組み合わせは、ハンバーガーメニューでも同じ考え方で使えます。実装例は CSSとJSだけで作るハンバーガーメニュー【コピペで使えるスニペット付き】 で紹介しています。
パターン9 — アニメーション(.animate() → CSS transition / Web Animations API)
jQueryの .animate({ opacity: 0 }, 300) のような書き方は、CSS transition + classList のほうがパフォーマンスが良いケースが多いです。ブラウザのレンダリング最適化に乗るぶん、カクつきにくくなります。
基本方針 — CSS寄せが第一
// jQuery
$('.modal').fadeOut(300);
// バニラJS(CSS + classList — 第一選択肢)
document.querySelector('.modal').classList.add('is-hidden');/* 第一選択肢: opacity + visibility + pointer-events の3点セット */
.modal {
opacity: 1;
visibility: visible;
pointer-events: auto;
transition: opacity 0.3s, visibility 0.3s;
}
.modal.is-hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}ハマりポイント — display: none にtransitionは効かない
display: none ⇄ display: block の切り替えはCSS transitionでは補間されません。代わりに opacity(フェード)+ visibility: hidden(スクリーンリーダーや Tab からも除外)+ pointer-events: none(クリック無効化) の3点セットで、「見えない・操作できない」状態を作ります。
細かく制御したいときはWeb Animations API
JSから細かく制御したいときは、element.animate(...) が使えます。
// Web Animations API
document.querySelector('.modal').animate(
[{ opacity: 1 }, { opacity: 0 }],
{ duration: 300, fill: 'forwards' }
);参考 — @starting-style という新しいCSS機能
最近のCSSには @starting-style があり、display: none からの出現アニメーションも書けるようになりつつあります。ただしブラウザ差異があるため、本記事では opacity + visibility を第一選択肢としています(詳細は別記事で改めて扱う予定です)。
パターン10 — フォーム値の取得(.val() → value / FormData)
フォーム値の取得は、対応さえ覚えてしまえば詰まらない領域です。
// jQuery
const name = $('#name').val();
const agree = $('#agree').prop('checked');
const gender = $('input[name="gender"]:checked').val();// バニラJS
const name = document.getElementById('name').value;
const agree = document.getElementById('agree').checked;
const gender = document.querySelector('input[name="gender"]:checked')?.value;ラジオやチェックボックスの「未選択」を想定して ?. を添えておくと安全です。
フォーム全体を送信するときは FormData
jQueryの $('form').serialize() に相当するのが FormData です。
const form = document.querySelector('form');
const formData = new FormData(form);
// そのまま fetch の body に渡せる
await fetch('/api/submit', {
method: 'POST',
body: formData,
});
// URLエンコード文字列にしたいときは
const query = new URLSearchParams(formData).toString();ハマりポイント — 数値inputでも .value は文字列
<input type="number"> でも、element.value は文字列を返します。数値として扱いたいときは Number(...) で変換するか、valueAsNumber プロパティを使います。
const input = document.getElementById('age');
const age1 = Number(input.value); // 文字列を数値化
const age2 = input.valueAsNumber; // 数値型で直接取れるここを忘れると「足し算したら文字列連結になった」というjQuery由来の詰まり方をしがちです。
パターン11 — 繰り返し処理($.each → forEach / for...of)
基本の対応
// jQuery — 配列
$.each(['a', 'b', 'c'], function (i, val) {
console.log(i, val); // 0 'a', 1 'b', 2 'c'
});
// jQuery — 要素群
$('li').each(function (i) {
console.log(i, this.textContent);
});// バニラJS — 配列
['a', 'b', 'c'].forEach((val, i) => {
console.log(i, val); // 0 'a', 1 'b', 2 'c'
});
// バニラJS — 要素群
document.querySelectorAll('li').forEach((li, i) => {
console.log(i, li.textContent);
});ハマりポイント — 引数の順序がjQueryと逆
jQuery出身者が一番やらかすポイントです。
- jQueryの
.each:(index, element)の順 - バニラJSの
forEach:(element, index)の順
失敗例デモ — 引数を逆に書いてしまうパターン
// NG: jQueryの癖で (i, val) と書いてしまうと…
['a', 'b', 'c'].forEach((i, val) => {
// i には実際の要素 'a' が入ってしまう(想定と逆)
console.log(i, val); // 'a' 0, 'b' 1, 'c' 2
});
// OK: 要素, indexの順で受け取る
['a', 'b', 'c'].forEach((val, i) => {
console.log(i, val); // 0 'a', 1 'b', 2 'c'
});変数名を (val, i) と役割がわかる名前で書く癖を付けておくと、このミスは起きにくくなります。
break したいときは for...of
forEach は break / return でループを止められません。早期に処理を止めたい場合は for...of を使います。
for (const val of ['a', 'b', 'c']) {
if (val === 'b') break;
console.log(val); // 'a' のみ出力
}NodeListを map したいとき
NodeListは forEach は使えますが map filter は使えないので、配列化します(パターン2のおさらい)。
const texts = [...document.querySelectorAll('.item')].map((el) => el.textContent);いつjQueryを使い続けていいか——「脱jQuery」は目的じゃない
jQueryを使い続けていい場面は、今もあります。大事なのは「なぜ選ぶか」を言語化できることです。
使い続けていい場面
- 既存のWordPressテーマ・プラグインがjQuery前提で動いている
WordPressコアは今もjQueryを読み込み続けています。テーマやプラグインがjQuery前提で書かれている場合、無理に剥がすと副作用が出やすいです - 既存の大規模コードベースで、部分書き換えのリスクが書き換えのメリットを上回る
「動いているものに手を入れない」という判断は、保守性の観点で立派な選択肢です - 古い外部APIや、IE対応が残っているレガシー案件
fetchや標準APIだと代替が面倒な場面では、jQueryのほうがシンプルに書けます - チームがjQueryで統一されている
1人だけバニラJSで書くと、逆に保守性を下げてしまうことがあります。「読める人が増える書き方」を選ぶ判断も大事です
逆に、使うべきでない場面
- 新規LPや軽量なコーポレートサイトで、他に理由なくjQueryを読み込む(表示速度を犠牲にするだけ)
- React・Vue・Svelteなどのフレームワーク内部
- jQueryでしかやっていない処理が1〜2箇所だけの場合(ライブラリを1つ削除できる)
道具は目的と状況で選ぶ
私は前職がSEだったこともあり、「システム全体を見てから道具を選ぶ」という発想が染みついています。jQueryもバニラJSも、同じように目的と状況に合わせて選ぶ道具です。$(...) と document.querySelector の両方が書ける——それ自体が、引き出しが増えたということです。
書き換え時のハマりどころベスト3
ここまでの内容から、特に事故になりやすい3つを再掲しておきます。この3つだけ気をつければ、書き換えの事故は8割減らせます。
thisが変わる問題(パターン3)
アロー関数で書いた瞬間、thisがリスナー要素を指さなくなります。迷ったらevent.currentTargetを使うのがおすすめです- NodeListは配列ではない問題(パターン2・11)
forEachは使えても、map/filterは配列化が必要です。[...nodeList]でひと手間加えましょう - 要素が見つからないと
nullが返る問題(パターン2)
jQueryは空のオブジェクトを返すので気づきにくいですが、バニラJSはエラーで止まります。?.を添えて防御的に書いておくと事故が減ります
まとめ——対応表を手元に、1ファイルずつ置き換えてみよう
この記事では、jQueryからバニラJSへの書き換えパターンを11個取り上げました。ポイントは以下のとおりです。
- jQuery ↔ バニラJSの11パターン対応表を手元に置けば、書き換えの大半は自力でできる
- 詰まりやすいのはイベント委譲・NodeList・
fetchのエラー処理・innerHTMLのXSSリスク slideToggleやanimateは CSS寄せが基本。JSで書く必要があるのは、細かい制御のときだけ- jQueryは悪ではない。状況で選べることが、エンジニアとしての実力につながる
次のアクションとして提案したいのは、小さなページ1つを選んでjQueryを外して書き直してみることです。壊れる場所が見つかることが、何より一番の学びになります。
完璧に書き換えなくて大丈夫。1ファイル分、1コンポーネント分から。私自身も、少しずつ引き出しを増やしてきました。小さく繰り返すことが、結局は一番の近道です。
関連記事
jQuery ↔ バニラJSの書き換えに慣れてきたら、CSS設計やUI実装の引き出しも一緒に広げておくと、案件対応がぐっとラクになります。
状態クラス(.is-open など)の命名や、CSSとJSの役割分担を整理したい方に。本記事のパターン4「クラス操作」と相性の良いCSS設計手法です。
本記事のパターン8「slideToggle相当」の応用例として、classList.toggle と aria-expanded を使った実装パターンをまとめています。
