eval() を使わずに 安全に・再現可能に・制御された形で動的な振る舞いを実現する方法 を、実用的なパターンとサンプルコードで見せます。初心者でも試せるよう、短く分かりやすい例を中心にします。
まとめ(先に結論)
安全に動的振る舞いを実現する代表的な方法は主に次の4つ:
- 関数レジストリ(キーで関数を選ぶ) — 最も簡単で安全。
- ミニ DSL(JSONなどで動作を記述して専用の「実行器」で解釈) — 柔軟で安全。
- シリアライズ+ファクトリ(状態を別に保存して関数を復元) — クロージャの代替。
- 厳格なホワイトリストを使った
new Function/解析 — 最終手段、注意が必要。
以降、コード例つきで説明します。
1) 関数レジストリ(推奨)
「ユーザーから来る値は関数名(キー)のみ。実際の関数はあらかじめ登録しておき選んで呼ぶ」方法です。eval をまったく使わず安全です。
// 関数レジストリを作る
const registry = {
add: (a, b) => a + b,
sub: (a, b) => a - b,
greet: (name) => `Hello ${name}`
};
// 例:外部入力(安全なのはキーだけ)
// 外部から来るのは "action"(文字列) と args(配列)
function handleRequest(action, args) {
const fn = registry[action];
if (typeof fn !== 'function') {
throw new Error('許可されていない操作です');
}
return fn(...args);
}
// 利用例
console.log(handleRequest('add', [5, 3])); // 8
console.log(handleRequest('greet', ['Halu'])); // "Hello Halu"
JavaScript利点:実装が簡単、ホワイトリスト化され安全。
欠点:事前に用意した操作しかできない(ただしこれが安全の源泉)。
2) ミニ DSL(JSON で動作を記述 → 安全なインタプリタで実行)
ユーザー入力は「やること」の記述(JSON)だけ。実行は自前の解釈器(interpreter)で行います。演算、条件、ループなど必要最小限を定義しておけば eval は不要。
例:簡単な算術 DSL を作る(安全に式を評価する)
// DSL 例: { op: "add", args: [1, {op:"mul", args:[2,3]}] } -> 1 + (2*3)
function run(node) {
if (typeof node === 'number') return node;
if (typeof node === 'string') return node; // ※文字列は別扱い
switch(node.op) {
case 'add': return run(node.args[0]) + run(node.args[1]);
case 'sub': return run(node.args[0]) - run(node.args[1]);
case 'mul': return run(node.args[0]) * run(node.args[1]);
case 'div': return run(node.args[0]) / run(node.args[1]);
default: throw new Error('未対応の操作:' + node.op);
}
}
// 利用
const expr = { op: 'add', args: [1, { op: 'mul', args: [2, 3] }] };
console.log(run(expr)); // 7
JavaScript利点:柔軟で安全に拡張可能。外部から送られた JSON をそのまま評価しても安全。
欠点:インタプリタを設計する手間がある(でもセキュリティの投資として価値大)。
3) シリアライズ + ファクトリ(クロージャの代替)
クロージャ(スコープに閉じた変数)をそのまま toString() で保存しても復元できません。代わりに「状態(データ)」と「どのファクトリ関数で復元するか」を保存する方法です。
// factory は状態を受け取って関数を返す
function makeAdder(base) {
return function(num) { return base + num; };
}
// シリアライズする(状態だけ保存)
const state = { kind: 'adder', base: 10 }; // ←外部に保存可能(JSON化)
const factoryRegistry = {
adder: (s) => makeAdder(s.base)
};
// 復元
const restored = factoryRegistry[state.kind](state);
console.log(restored(5)); // 15
JavaScriptここでは toString() は使わない。状態(base: 10)を保存し、どの工場(adder)で復元するかを指定するだけです。
利点:クロージャの状態を安全に永続化/復元できる。
欠点:状態と工場(factory)を用意する必要があるが、安全第一。
4) ホワイトリストを使った限定的な new Function(要注意)
どうしても文字列から関数を作る必要がある場合、非常に慎重にホワイトリストや静的解析を入れて制限します。推奨しませんが、現実的な妥協策です。
ポイント:
- 許可されるコードパターンを限定(例:単純な式のみ、API 呼び出し禁止)
- 必ず正規表現やパーサで検査して安全でなければ拒否
- サンドボックス化(別プロセス・Worker)で実行することを検討
簡単な検査例(ただし正規表現だけで完全に安全にするのは難しい):
const allowedPattern = /^[0-9+\-*/\s()a-zA-Z,]+$/;
function safeCreate(argNames, body) {
if (!allowedPattern.test(body)) throw new Error('不許可の文字が含まれる');
return new Function(...argNames, body);
}
JavaScript注意:これは例示用。現実にはもっと厳格なパーシングが必要です。
例:先ほどのクロージャデモを「安全に」置き換える
先のデモでは add10.toString() を eval で復元して失敗しました。状態を保存してファクトリで復元する例:
// 以前: const code = add10.toString(); // NG
// 新しい安全設計:状態を保存してファクトリで復元
function makeAdder(base) { return function(num){ return base + num; }; }
// 状態として保存
const saved = { type: 'adder', base: 10 }; // JSON.stringify(saved) で保存可能
// 復元ロジック(ファクトリレジストリ)
const factories = {
adder: s => makeAdder(s.base)
};
const restored = factories[saved.type](saved);
console.log(restored(5)); // 15 ← 正しく再現できる
JavaScript実務でのベストプラクティス
- ユーザー入力は「コード」ではなく「データ(キー / パラメータ / JSON)」で受ける。
- 実行するロジックはサーバー側か事前登録された関数(レジストリ)で安全に管理。
- どうしても文字列評価が要る場合は厳重に検査・サンドボックス化する(そしてその必要性を再検討する)。


