はじめに (対象読者・この記事でわかること)
この記事は、JavaScript の基礎は理解しているが、変数への代入操作を「何かしらの形で保存したい」――例えば代入の履歴を残したり、後から同じ代入を再実行したりしたい――と考えているエンジニアを対象としています。読了後には、代入式を関数として抽象化する方法、Proxy を用いた代入のインターセプト、そしてそれらを実務で活かす具体的なコード例がすぐに書けるようになります。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- ES6 以降の let / const / var の基本的な使い方
- 関数(特にアロー関数)とクロージャの概念
- オブジェクトリテラルとプロパティ操作の基礎
1. 変数代入を保存したい背景と基本概念
JavaScript では変数へ値を代入するたびに、実行時にメモリ上の値が上書きされます。単純に「代入した値は残したい」だけなら、別の変数にコピーすれば済みますが、「代入そのもののロジック」 を保存したい場面があります。
- 履歴管理: ユーザーが入力した値の変更履歴を保持し、Undo/Redo に利用したい。
- デバッグ支援: ある変数にいつ、どの値が代入されたかを自動でログに残したい。
- 再利用: 複数箇所で同じ代入ロジック(例: 計算結果を代入)を共有したい。
このような要件に対して、JavaScript では「代入ロジックを関数として抽象化」するか、Proxy で代入操作をフックするという二つの主なアプローチがあります。以下では両方の手法を実装例とともに詳しく見ていきます。
2. 具体的な手順や実装方法
2‑1. 関数で代入ロジックをカプセル化する
代入自体を関数に閉じ込めると、必要なときにその関数を呼び出すだけで同じ代入が再現できます。シンプルな例として、数値を二乗した結果を変数 result に代入するロジックを保存します。
Js// 代入ロジックを保持する関数ファクトリ function createAssignment(targetObj, prop) { // 戻り値は代入を実行する関数 return function (value) { targetObj[prop] = value; console.log(`[assign] ${prop} ← ${value}`); }; } // 使用例 const state = { result: 0 }; const assignResult = createAssignment(state, "result"); // 任意のタイミングで代入 assignResult(5); // => result = 5 assignResult(5 * 5); // => result = 25
ポイントは次の通りです。
1. 対象オブジェクトとプロパティ名 を引数に取ることで、どの変数でも同じ関数で処理できる汎用性を持たせる。
2. 関数内部で console.log を入れることで、代入のたびに自動的にデバッグ情報が出力されます。
3. assignResult は普通の関数なので、setTimeout や Promise のチェーンでも自由に利用可能です。
応用:計算付き代入
計算ロジックを組み込みたい場合は、関数をさらにラップします。
Jsfunction createComputedAssignment(targetObj, prop, computeFn) { return function (...args) { const value = computeFn(...args); targetObj[prop] = value; console.log(`[computed assign] ${prop} ← ${value}`); }; } // 例: 二乗を計算して代入 const assignSquare = createComputedAssignment(state, "result", x => x * x); assignSquare(3); // result = 9 assignSquare(4); // result = 16
2‑2. Proxy で代入操作をインターセプトする
関数で代入ロジックを保存する方法は明示的な呼び出しが必要です。もっと透明に「代入した瞬間」に処理を走らせたいなら、Proxy が便利です。Proxy はオブジェクトへのアクセス(取得・設定)をフックできるので、代入が行われたときに自動でコールバックを走らせられます。
Jsfunction createAssignWatcher(targetObj, onAssign) { return new Proxy(targetObj, { set(target, prop, value, receiver) { // 代入前に任意の処理を実行 onAssign(prop, value); // 実際の代入 return Reflect.set(target, prop, value, receiver); } }); } // 使用例 const state2 = { count: 0 }; const watchedState = createAssignWatcher(state2, (prop, value) => { console.log(`[watch] ${prop} が ${value} に変更`); }); watchedState.count = 1; // ログが出力され、代入が反映される watchedState.count = watchedState.count + 5; // 6 に変更、同様にログ
履歴管理を組み込む
onAssign に履歴配列へのプッシュ処理を入れれば、Undo/Redo が実装できます。
Jsfunction createHistoryProxy(targetObj) { const history = []; const proxy = new Proxy(targetObj, { set(target, prop, value, receiver) { history.push({ prop, oldValue: target[prop], newValue: value }); console.log(`[history] ${prop}: ${target[prop]} → ${value}`); return Reflect.set(target, prop, value, receiver); }, get(target, prop, receiver) { if (prop === "_history") return history.slice(); // 履歴のコピーを取得 return Reflect.get(target, prop, receiver); } }); return proxy; } // 使用例 const model = createHistoryProxy({ x: 0, y: 0 }); model.x = 10; model.y = 20; console.log(model._history); // => [ // { prop: 'x', oldValue: 0, newValue: 10 }, // { prop: 'y', oldValue: 0, newValue: 20 } // ]
2‑3. ハマりやすいポイントとエラー対策
| 項目 | よくある問題 | 回避・解決策 |
|---|---|---|
| クロージャのスコープ | createAssignment 内で targetObj を参照し忘れ、外部の同名変数を参照してしまう。 |
関数定義時に必ず targetObj を引数に取り、strict mode を有効にする。 |
Proxy の非互換 |
古いブラウザ(IE)では Proxy が未実装。 |
必要に応じて Babel の polyfill または代替実装(Object.defineProperty)を利用。 |
| 再代入と参照の混同 | オブジェクトそのものを別変数に代入しただけだと、参照が共有されているため片方を変更するともう片方も変わる。 | コピーが必要な場合は Object.assign({}, obj) やスプレッド構文 {...obj} を使う。 |
| 無限ループ | Proxy の set ハンドラ内で同じプロキシに対して代入を行うと再帰的に呼び出され、スタックオーバーフローになる。 |
Reflect.set を使用して実際の代入を行うか、代入対象を別オブジェクトに切り替える。 |
2‑4. 実務での活用例
-
フォーム入力の変更履歴
UI フレームワーク(React/Vue)と組み合わせて、入力データオブジェクトをProxyで包み、onAssignでローカルストレージへ自動保存。Undo ボタンは履歴配列を逆順にたどるだけで実装可能。 -
テストコードの自動記録
テスト対象のオブジェクトにProxyをかませ、テスト実行中の全代入をログファイルに出力。テスト失敗時に「実際にどの値が代入されたか」を即座にトレースできる。 -
計算ロジックの再利用
ビジネスロジックで頻出する「A を計算して B に代入」パターンは、createComputedAssignmentを使えば関数だけで完結。コードの重複を削減し、変更があった際は一箇所だけ修正すれば済む。
まとめ
本記事では、JavaScript における「変数への代入操作を別の変数や構造に保存する」テクニックとして、① 関数で代入ロジックをカプセル化する方法、② Proxy を利用した代入のインターセプトと履歴管理、③ 実装上の落とし穴とその回避策、そして実務での応用例を紹介しました。
- 代入ロジックを関数化 すれば、再利用とデバッグがシンプルになる。
Proxyは代入瞬間にフックでき、履歴や副作用の実装に最適。- スコープや無限ループ などの典型的な落とし穴を把握しておけば、実装時のバグを大幅に減らせる。
これらの手法を活用すれば、コードの可観測性が向上し、保守性の高いアプリケーションを構築しやすくなります。次回は、Proxy と非同期処理(async/await)を組み合わせた高度なデバッグパターンについて掘り下げる予定です。
参考資料
- MDN Web Docs – Proxy
- JavaScript Garden – Closures
- 「Effective JavaScript」 – David Herman
- 「You Don't Know JS Yet」 – Kyle Simpson, 第3巻 「this & Object Prototypes」