バニラJSのモダンな書き方・チートシート

著者画像
Toshihiko Arai

sessionStorage / localStorageでデータ保存

sessionStorage.setItem('myData', '保存したい内容'); // 保存
const savedData = sessionStorage.getItem('myData'); // 読み出す
sessionStorage.removeItem('myData'); // 削除
sessionStorage.clear(); // 全て削除

sessionStorage は、同一タブまたは同一ウィンドウでのみデータ保存・維持されます。つまりタブを閉じるとデータは破棄。最大容量はブラウザに依存ですが、おおむね5MB。また、保存形式は文字列のみ。オブジェクトを保存したい場合は JSON.stringify() で文字列にしてから保存するなどの工夫が必要です。

sessionStoragelocalStorage へ書き換えるとデータを永続化できます。

Object.freeze で凍結

const Trial = {
  ok: true,
  ng: false
};

Object.freeze(Trial);   // オブジェクトを凍結
Trial.ok = false;       // エラーにはならないが値は変わらない
console.log(Trial.ok);  // true

Trialconst で宣言されてますが、プロパティ内の値までは不変になりません。オブジェクトの入れ子まで伝播して変更不可にしたい場合に Object.freeze() を使います。

JSで Enum 表現したい場合などに使えます。

fetch / async / await 非同期処理

async function fetchUser(userId) {
  const response = await fetch(`/api/user/${userId}`);
  if (!response.ok) throw new Error("通信エラー");
  const data = await response.json();
  return data;
}

fetchUser(1).then(user => {
  console.log(user.name);
}).catch(err => {
  console.error(err);
});

fetch() はサーバーと非同期通信するための組み込みAPIです。await は非同期処理が完了するまで処理を待機させることができます。Promiseを使うよりもawaitを使った方が、コールバックヘルを避けられると思ってます。

await を使う場合は親の関数を async で宣言しておく必要があります。

Promise 非同期処理

function wait(ms) {
  return new Promise(resolve => {
    setTimeout(() => resolve(`${ms}ms 待ちました`), ms);
  });
}

wait(1000).then(msg => console.log(msg));

Promise は非同期処理の結果を表すオブジェクトです。resolve で成功、reject で失敗を返します。

then 文結果を Promise で返せば、then の数珠繋ぎで非同期処理を連結できます。

filter / map で配列処理

const nums = [1, 2, 3, 4, 5];
const even = nums.filter(n => n % 2 === 0); // [2, 4]
const squared = nums.map(n => n * n);       // [1, 4, 9, 16, 25]

console.log(even, squared);

filter / map は配列を加工するメソッドです。ちょっとした処理なら、for文を使わずにワンライナーで処理をかけるのが魅力です。

every / some で配列の条件チェック

// every: 全ての要素が条件を満たすか?
const allPositive = [1, 2, 3].every(n => n > 0); // true

// some: 一つでも条件を満たせばOK
const hasNegative = [1, -2, 3].some(n => n < 0); // true

everysome は配列の要素に対して条件チェックをするメソッドです。filter と違って、真偽値だけ返すメソッドです。

... スプレッド構文

const a = [1, 2];
const b = [3, 4];
const merged = [...a, ...b]; // [1, 2, 3, 4]

... の3点リーダーはスプレッド構文で、配列やオブジェクトを便利に展開できます。

Object.assign で結合

// マージ
const a = { x: 1 }, b = { y: 2 };
Object.assign(a, b);           // a は { x:1, y:2 } に上書きされる

// デフォルト適用(後ろが優先)
const opts = Object.assign({ timeout: 5000, retry: 0 }, userOpts);

// 浅いクローン(target を {} に)
const clone = Object.assign({}, source);

Object.assign(target, ...sources) は、列挙可能な自前プロパティ(文字列キーと Symbol)を左から右へ浅くコピーして target破壊的に更新し、その target を返します。

スプレッド構文でも、同じことができます。

// ほぼ同じ挙動(どちらも浅いコピー)
const merged = { ...a, ...b };
const clone2 = { ...source };

浅いコピーでは、入れ子のオブジェクトは参照が共有されます。

structuredClone で深いコピー

const src = { a: 1, d: new Date(), m: new Map([['k', 1]]) };
const dst = structuredClone(src);

structuredClone は ブラウザ/Node に内蔵の深いコピーAPIですstructuredClone(value, options?) で、オブジェクトグラフを再帰的に複製します。

浅いコピー と 深いコピー で挙動がどう違うのか? 次の例で理解できます。

// 浅いコピー(Object.assign / スプレッド)
const src = { nest: { x: 1 } };
const shallow = { ...src };         // or Object.assign({}, src)
shallow.nest.x = 9;
console.log(src.nest.x);            // 9 ← 参照が共有される

// 深いコピー(structuredClone)
const deep = structuredClone(src);
deep.nest.x = 7;
console.log(src.nest.x);            // 1 ← 独立

JSの引数:プリミティブは値、オブジェクトは参照の“コピー”

ここでJavaScriptの関数の引数の挙動を確認しておきます。JSは常に値渡しになりますが、オブジェクトの場合は参照のコピーが渡され、同じ実体を共有するので注意が必要です。つまりこうです。

共有ゆえに外側へ反映される例

function modifyObject(obj) {
  obj.value = 'fuga';
}

const myObject = { value: 'hoge' };
console.log(myObject.value); // "hoge"
modifyObject(myObject);
console.log(myObject.value); // "fuga" ← 中身を書き換えると外側に反映

ただし再代入は外側に影響しない

function reassign(obj) {
  obj = { value: 'bar' }; // 参照の“受け取り側”を別物に差し替えただけ
}

const o = { value: 'hoge' };
reassign(o);
console.log(o.value); // "hoge"

プリミティブは常に値のコピー

function modifyPrimitive(v) {
  v = 10;
}

let x = 5;
modifyPrimitive(x);
console.log(x); // 5

マージや浅い複製には Object.assign / スプレッド、完全な複製には structuredClone を使い分けると安全です。

class

class User {
  constructor(name) {
    this.name = name;
  }
  greet() {
    return `Hello, ${this.name}`;
  }
}

const u = new User("Alice");
console.log(u.greet());

class は ES6 から使えるようになったのクラス構文です。

trace new Error(呼び出し元の関数を知る)

function debugTrace() {
  console.log(new Error("trace").stack);
}
Error: trace
    at debugTrace (article_form.js:26:15)
    at initViews (article_form.js:33:3)
    at HTMLDocument.<anonymous> (article_form.js:21:3)
    at e (jquery-3.7.1.min.js:2:27028)
    at t (jquery-3.7.1.min.js:2:27330)

new Error().stack で呼び出し元をトレースできます。

JSで呼び出し元関数を知るには、このトリッキーな方法以外に知らないです。。

datasetプロパティでdata属性へアクセス

<div id="user" data-id="123" data-role="admin"></div>
const el = document.getElementById("user");

// 取得
console.log(el.dataset.id);   // "123"
console.log(el.dataset.role); // "admin"

// 設定
el.dataset.role = "editor";

// 削除
delete el.dataset.id;

HTML側では data-xxx と書き、JSでは camelCase(例: data-user-namedataset.userName)でアクセスします。文字列として保存されるため、数値や真偽値として使うときは変換が必要です。

軽量なメタ情報の埋め込みに便利ですが、複雑な状態管理には使いすぎない方が良いです。

JSON.stringify()/JSON.parse() でJSON変換

//オブジェクトをJSON文字列に変換
const obj = { name: "Alice", age: 25 };
const json = JSON.stringify(obj);
console.log(json); // {"name":"Alice","age":25}

// JSON文字列をオブジェクトに戻す
const json = '{"name":"Alice","age":25}';
const obj = JSON.parse(json);
console.log(obj.name); // Alice

JSON.stringify()JSON.parse() は JavaScript でオブジェクトを文字列化・復元するための基本メソッドです。

Map / Set コレクション

Mapキーと値のペアを保持するコレクションです。順序は挿入順に保持されます。

const map = new Map();
map.set("name", "Taro");
console.log(map.get("name")); // "Taro"

キーに オブジェクト も使えます。

const obj = { id: 1 };
const map = new Map();
map.set(obj, "Hanako");
console.log(map.get(obj)); // "Hanako"

Set重複しない値のコレクションです。配列と違って同じ値を二度追加できません。

const set = new Set([1, 2, 2, 3]);
console.log(set); // Set(3) {1, 2, 3}

set.add(4);
console.log(set.has(2)); // true

WeakMap / WeakSet でメモリ管理

WeakMapオブジェクトをキーにしたマップですが、キーは必ずオブジェクトの必要があります。キーとなるオブジェクトが他で参照されなくなると 自動的に削除される(ガーベジコレクション対象)特徴があります。

参照が切れると自動削除されるため、キャッシュや一時データに便利です。ただし列挙はできません。

let obj = { name: "Taro" };
const weakMap = new WeakMap();
weakMap.set(obj, "data");

console.log(weakMap.get(obj)); // "data"
obj = null; // 参照がなくなると weakMap 内からも消える

WeakSetオブジェクトだけを格納する Set です。要素が他で参照されなくなれば、自動的に削除されます。

let obj = { id: 1 };
const weakSet = new WeakSet();
weakSet.add(obj);

console.log(weakSet.has(obj)); // true
obj = null; // 他から参照されないと自動削除される

ガベージコレクションなので削除されるタイミングは保証されません。

MutationObserver で DOM の変更を監視

MutationObserverとは

MutationObserver は DOM(HTML要素)の変化を監視できるブラウザAPIです。監視対象の要素に子要素の追加・削除、属性の変更、テキストの変更などがあった時にコールバックを実行してくれます。

// 1. コールバック関数を定義
const callback = (mutationsList, observer) => {
  mutationsList.forEach(mutation => {
    console.log(mutation.type); // 変化の種類を確認
  });
};

// 2. オブザーバーを生成
const observer = new MutationObserver(callback);

// 3. 監視対象を取得
const targetNode = document.getElementById("nuContents");

// 4. 監視オプションを指定して開始
observer.observe(targetNode, {
  childList: true,       // 子要素の追加・削除
  attributes: true,      // 属性の変更
  characterData: true,   // テキスト内容の変更
  subtree: true          // 子孫要素も監視
});

このようにして特定のDOMにオブザーバー割り当て、DOMに変更があった場合に通知(コールバック)されます。次のように実際にDOMを変更してみましょう。コールバックが呼び出されるはずです。

// 子要素を追加
const newEl = document.createElement("p");
newEl.textContent = "Hello!";
targetNode.appendChild(newEl);

// 属性を変更
targetNode.setAttribute("data-status", "updated");

jQuery拡張としての実装

この仕組みを応用して、本来実装されていないhidden値の変更を監視できるように jQuery を拡張してみました。

(function($) {
  $.fn.observeValue = function(callback) {
    return this.each(function() {
      if (this.tagName.toLowerCase() !== 'input' || this.type !== 'hidden') {
        throw new Error('observeValue(): 対象は input[type="hidden"] のみです');
      }
      var observer = new MutationObserver(function(mutations, obs) {
        callback.call(this, mutations, obs);
      }.bind(this));
      observer.observe(this, { attributes: true, attributeFilter: ["value"] });
    });
  };
})(jQuery);

次のようにして使うことができます。

// 単一要素
$("#myHidden").observeValue(function() {
  console.log("hiddenの値が変更されました:", this.value);
});

// 複数要素
$("#myHidden, #yourHidden").observeValue(function() {
  console.log("hiddenの値が変更されました:", this.value);
});

Amazonで探す