$('.js-menu').on('click', ...) はスラスラ書けるのに、案件で「jQuery使わないでほしい」と言われた瞬間、手が止まる——。

オンラインスクールや学習教材でjQueryから入った方なら、似た経験があるのではないでしょうか。私自身もそうでした。$(...) が手に染みついていて、バニラJSに置き換えると「this が消えた」「NodeListが map できない」と小さなつまずきが積み重なります。

この記事では、以下の3つを持ち帰れるように書いています。

  • jQuery ↔ バニラJSの対応表で、脳内マップを作れる
  • 11パターン分のコード例をコピペして試せる
  • 今でもjQueryを使っていい場面がわかる(=使い分けの判断軸が手に入る)

私自身、案件によっては今もjQueryを書きます。大事なのは道具を状況で選べることです。この記事も、どちらかを貶める内容ではありません。

なお、11パターンすべてを順に読む必要はありません。次の対応表から、いま手元で詰まっているパターンを拾い読みしてOKです。

そもそも、なぜ今バニラJSに書き換えるのか——「jQueryは悪」ではない

最初にはっきりさせておきたいのは、jQueryは悪ではないということです。2010年代のフロントエンドを支えた偉大なライブラリで、今もWordPressコアが読み込み続けているくらい現役です。「jQueryから入った自分は遅れている」と感じる必要はありません。

ただ、ここ数年で以下のような変化が積み重なってきました。

  • モダンブラウザの標準API(querySelectorclassListfetch)が十分に成熟した
  • 表示速度・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 / hasClassclassList.add / remove / toggle / contains
パターン5属性操作attr() / prop() / data()getAttribute / setAttribute / dataset
パターン6DOM生成・削除append / html / text / removeinsertAdjacentHTML / appendChild / createElement / innerHTML / textContent / .remove()
パターン7Ajax通信$.ajax / $.get / $.postfetch
パターン8slideToggle相当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準備完了待ち」の処理です。

JavaScript
// jQuery — 2種類の書き方は同じ意味
$(document).ready(function () {
  console.log('DOM ready');
});

$(function () {
  console.log('DOM ready');
});

バニラJSでは DOMContentLoaded イベントを使います。

JavaScript
// バニラJS
document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM ready');
});

実は、書かなくていい場合もある

読み込みタイミング処理は、スクリプトの置き場所次第で不要になります。

  • <script></body> の直前に置く → DOM生成後に実行される
  • <script defer src="..."> で読み込む → HTMLパース後に実行される

私は保守性の観点で defer を好んで使います。「いつ実行されるか」が属性で明示されるので、後から読む人が迷いません。

ハマりポイント — すでに読み込み完了後にリスナーを登録するケース

動的にスクリプトを差し込む場合など、DOMが既に準備できているタイミングで DOMContentLoaded を登録するとイベントが発火しません。readyState で分岐するのが安全です。

JavaScript
// 「もう準備できているなら即実行、そうでないなら待つ」の安全パターン
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else {
  init();
}

function init() {
  console.log('DOM ready');
}

パターン2 — 要素の取得($('.foo')querySelector / querySelectorAll

いちばん使う操作です。jQueryの $() は単一・複数どちらもカバーしましたが、バニラJSでは呼び分けます。

基本の対応

JavaScript
// jQuery
const $el = $('#main');
const $items = $('.item');
JavaScript
// バニラ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 で配列化してから使います。

JavaScript
// バニラ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 を返します。そのままメソッドを呼ぶとエラーです。

JavaScript
// 見つからないケースを想定して防御的に書く
const modal = document.querySelector('.js-modal');
modal?.classList.add('is-open');

失敗例デモ — null を無視して呼ぶ

JavaScript
// セレクタにタイプミスがあったり、まだ要素が存在しない場合
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

基本の対応

JavaScript
// jQuery
$('.btn').on('click', function (e) {
  console.log('clicked');
});
JavaScript
// バニラJS
const btn = document.querySelector('.btn');
btn.addEventListener('click', (e) => {
  console.log('clicked');
});

e.preventDefault()e.stopPropagation()jQueryでもバニラでも同じなので、そこは安心してください。

イベント委譲はバニラだとどう書くか

jQueryで頻出のイベント委譲(動的生成の要素にまとめてイベントを付ける書き方)は、学習者が一番詰まるところです。

JavaScript
// jQuery(委譲)
$('.list').on('click', '.item', function (e) {
  console.log(this.textContent);
});

バニラJSでは、親要素にリスナーを付けて closest で対象を絞るのが定番です。

JavaScript
// バニラ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 を使う

JavaScript
// 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 — クラス操作(addClassclassList

実装で頻繁に使う操作です。対応を覚えれば、機械的に置き換えられます。

対応表

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')
JavaScript
// jQuery
$('.menu').addClass('is-open');
$('.menu').removeClass('is-open');
$('.menu').toggleClass('is-open');
if ($('.menu').hasClass('is-open')) { /* ... */ }
JavaScript
// バニラ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は可変長引数で渡します。

JavaScript
// 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
JavaScript
// jQuery
const href = $('.link').attr('href');
$('.link').attr('href', '/new');

const checked = $('.agree').prop('checked');

const id = $('.btn').data('userId'); // data-user-id属性
JavaScript
// バニラ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.userId

data-user-id のようにハイフンを含む属性は、dataset.userIdキャメルケースに変換されます(jQueryの .data('userId') と似た感覚)。

ハマりポイント — dataset は常に文字列(型の違い)

  • jQuery .data() は「型推論」をします。数値っぽい文字列は数値に、JSON文字列はオブジェクトに、自動変換されます
  • バニラJS dataset常に文字列を返します
HTML
<button class="js-btn" data-user-id="42" data-role="admin">送信</button>
JavaScript
// jQuery — 型推論する
const id1 = $('.js-btn').data('userId'); // 42(数値として返る)

// バニラJS — 常に文字列
const btn = document.querySelector('.js-btn');
const id2 = btn.dataset.userId; // "42"(文字列)

失敗例デモ — 「永遠にfalse」になるパターン

JavaScript
// 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()安全
JavaScript
// jQuery
$('.title').text('こんにちは');
$('.list').append('<li class="item">新規</li>');
$('.item').remove();
JavaScript
// バニラ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発火パターン

JavaScript
// 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 / $.postfetch

jQueryの $.ajaxfetch に置き換えるパターンです。WordPress案件では送信形式を2種類使い分けるため、ここは少し長めに見ていきます。

基本の書き換え(async / await)

JavaScript
// 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),
});
JavaScript
// バニラ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() を呼ぶ
JavaScript
// 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)
JavaScript
// 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)
JavaScript
// 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つです。

  1. CSS transition + classList.toggle(推奨)
  2. <details> / <summary> タグ(セマンティクス重視)
  3. Web Animations API(細かく制御したいとき)

1. CSS transition + classList.toggle

もっとも汎用的なパターンです。定番は max-height のtransition。

HTML
<button class="js-toggle" type="button" aria-expanded="false" aria-controls="panel">
  開閉
</button>
<div id="panel" class="panel">
  <p>折りたたまれるコンテンツ</p>
</div>
CSS
.panel {
  overflow: hidden;
  max-height: 0;
  transition: max-height 0.3s ease;
}
.panel.is-open {
  max-height: 500px; /* 中身より大きい値。中身が超えると見切れるので、可変コンテンツなら grid-template-rows トリックを検討 */
}
JavaScript
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-expandedaria-controls を添えておくと、スクリーンリーダー利用者にも開閉状態が伝わります。aria-controls の値は対象要素の id と一致させるのがルール(上の例では id="panel"aria-controls="panel")。

もうひとつの書き方 — grid-template-rows: 0fr → 1fr トリック

2024年頃から広まった新しい書き方として、grid-template-rows0fr1fr でtransitionする方法もあります。高さが可変なコンテンツに強いのが利点ですが、比較的新しい手法なので案件要件次第では採用判断が分かれます。古めの環境では max-height のほうが無難です。

2. <details> / <summary> タグ

アコーディオンやFAQのようなUIなら、HTML標準の <details> タグが一番ラクです。

<details class="faq">
  <summary>よくある質問です</summary>
  <p>回答の本文です。</p>
</details>

最大の利点は、キーボード操作とスクリーンリーダー対応が自動で効くこと。JSなしでアクセシブルなアコーディオンが作れるので、要件に合うなら第一選択肢にしても良いと思います。

ハンバーガーメニューへの応用

classList.togglearia-expanded の組み合わせは、ハンバーガーメニューでも同じ考え方で使えます。実装例は CSSとJSだけで作るハンバーガーメニュー【コピペで使えるスニペット付き】 で紹介しています。

パターン9 — アニメーション(.animate() → CSS transition / Web Animations API)

jQueryの .animate({ opacity: 0 }, 300) のような書き方は、CSS transition + classList のほうがパフォーマンスが良いケースが多いです。ブラウザのレンダリング最適化に乗るぶん、カクつきにくくなります。

基本方針 — CSS寄せが第一

JavaScript
// jQuery
$('.modal').fadeOut(300);

// バニラJS(CSS + classList — 第一選択肢)
document.querySelector('.modal').classList.add('is-hidden');
CSS
/* 第一選択肢: 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: nonedisplay: block の切り替えはCSS transitionでは補間されません。代わりに opacity(フェード)+ visibility: hidden(スクリーンリーダーや Tab からも除外)+ pointer-events: none(クリック無効化) の3点セットで、「見えない・操作できない」状態を作ります。

細かく制御したいときはWeb Animations API

JSから細かく制御したいときは、element.animate(...) が使えます。

JavaScript
// 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

フォーム値の取得は、対応さえ覚えてしまえば詰まらない領域です。

JavaScript
// jQuery
const name = $('#name').val();
const agree = $('#agree').prop('checked');
const gender = $('input[name="gender"]:checked').val();
JavaScript
// バニラ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 です。

JavaScript
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 プロパティを使います。

JavaScript
const input = document.getElementById('age');

const age1 = Number(input.value); // 文字列を数値化
const age2 = input.valueAsNumber; // 数値型で直接取れる

ここを忘れると「足し算したら文字列連結になった」というjQuery由来の詰まり方をしがちです。

パターン11 — 繰り返し処理($.eachforEach / for...of

基本の対応

JavaScript
// 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);
});
JavaScript
// バニラ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) の順

失敗例デモ — 引数を逆に書いてしまうパターン

JavaScript
// 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

forEachbreak / return でループを止められません。早期に処理を止めたい場合は for...of を使います。

JavaScript
for (const val of ['a', 'b', 'c']) {
  if (val === 'b') break;
  console.log(val); // 'a' のみ出力
}

NodeListを map したいとき

NodeListは forEach は使えますが map filter は使えないので、配列化します(パターン2のおさらい)。

JavaScript
const texts = [...document.querySelectorAll('.item')].map((el) => el.textContent);

いつjQueryを使い続けていいか——「脱jQuery」は目的じゃない

jQueryを使い続けていい場面は、今もあります。大事なのは「なぜ選ぶか」を言語化できることです。

使い続けていい場面

  1. 既存のWordPressテーマ・プラグインがjQuery前提で動いている
    WordPressコアは今もjQueryを読み込み続けています。テーマやプラグインがjQuery前提で書かれている場合、無理に剥がすと副作用が出やすいです
  2. 既存の大規模コードベースで、部分書き換えのリスクが書き換えのメリットを上回る
    「動いているものに手を入れない」という判断は、保守性の観点で立派な選択肢です
  3. 古い外部APIや、IE対応が残っているレガシー案件
    fetch や標準APIだと代替が面倒な場面では、jQueryのほうがシンプルに書けます
  4. チームがjQueryで統一されている
    1人だけバニラJSで書くと、逆に保守性を下げてしまうことがあります。「読める人が増える書き方」を選ぶ判断も大事です

逆に、使うべきでない場面

  • 新規LPや軽量なコーポレートサイトで、他に理由なくjQueryを読み込む(表示速度を犠牲にするだけ)
  • React・Vue・Svelteなどのフレームワーク内部
  • jQueryでしかやっていない処理が1〜2箇所だけの場合(ライブラリを1つ削除できる)

道具は目的と状況で選ぶ

私は前職がSEだったこともあり、「システム全体を見てから道具を選ぶ」という発想が染みついています。jQueryもバニラJSも、同じように目的と状況に合わせて選ぶ道具です。$(...)document.querySelector の両方が書ける——それ自体が、引き出しが増えたということです。

書き換え時のハマりどころベスト3

ここまでの内容から、特に事故になりやすい3つを再掲しておきます。この3つだけ気をつければ、書き換えの事故は8割減らせます。

  1. this が変わる問題(パターン3)
    アロー関数で書いた瞬間、this がリスナー要素を指さなくなります。迷ったら event.currentTarget を使うのがおすすめです
  2. NodeListは配列ではない問題(パターン2・11)
    forEach は使えても、map / filter は配列化が必要です。[...nodeList] でひと手間加えましょう
  3. 要素が見つからないと null が返る問題(パターン2)
    jQueryは空のオブジェクトを返すので気づきにくいですが、バニラJSはエラーで止まります。?. を添えて防御的に書いておくと事故が減ります

まとめ——対応表を手元に、1ファイルずつ置き換えてみよう

この記事では、jQueryからバニラJSへの書き換えパターンを11個取り上げました。ポイントは以下のとおりです。

  • jQuery ↔ バニラJSの11パターン対応表を手元に置けば、書き換えの大半は自力でできる
  • 詰まりやすいのはイベント委譲・NodeList・fetch のエラー処理・innerHTML のXSSリスク
  • slideToggleanimateCSS寄せが基本。JSで書く必要があるのは、細かい制御のときだけ
  • jQueryは悪ではない状況で選べることが、エンジニアとしての実力につながる

次のアクションとして提案したいのは、小さなページ1つを選んでjQueryを外して書き直してみることです。壊れる場所が見つかることが、何より一番の学びになります。

完璧に書き換えなくて大丈夫。1ファイル分、1コンポーネント分から。私自身も、少しずつ引き出しを増やしてきました。小さく繰り返すことが、結局は一番の近道です。

関連記事

jQuery ↔ バニラJSの書き換えに慣れてきたら、CSS設計やUI実装の引き出しも一緒に広げておくと、案件対応がぐっとラクになります。

状態クラス(.is-open など)の命名や、CSSとJSの役割分担を整理したい方に。本記事のパターン4「クラス操作」と相性の良いCSS設計手法です。

本記事のパターン8「slideToggle相当」の応用例として、classList.togglearia-expanded を使った実装パターンをまとめています。