eval() なしで JavaScript 計算機を作る
JavaScript でコードを書いていると、動的にコードを実行する必要がある場面が出てきます。多くの開発者はそのような時に eval() 関数を思い浮かべるでしょう。eval() は文字列をコードとして認識し実行してくれる便利な機能です。しかし、この便利さの裏には重大なセキュリティリスクとパフォーマンス低下の問題が潜んでいます。
この記事では、eval() を使うことがなぜ危険なのかを見ていき、より安全で効率的な代替手段である new Function() を使って動的にコードを実行する方法を詳しく見ていきます。この記事を通じて、皆さんはより安全で堅牢なコードを作る力を養うことができるでしょう。
eval() とは何で、なぜ危険なのでしょうか?
eval() は渡された文字列を JavaScript コードとして解釈し、実行するグローバル関数です。例えば、eval("2 + 2") は数値 4 を返します。このように、文字列で表現されたコードを動的に実行できるため、計算機ロジックなどを実装する際には有用に見えます。
しかし eval() の使用には、いくつかの深刻な問題が発生する可能性があります。
1. セキュリティ脆弱性
eval() の最大の問題はセキュリティです。この関数は呼び出されたスコープの権限でコードを実行してしまうため、もしユーザーが入力した値を検証せずに eval() で実行すると、悪意あるコードがそのまま実行されてしまうおそれがあります。
例えば、ユーザーが入力した値を eval() で処理するコードがあったと仮定しましょう。
var userContent = getUserInput(); // ユーザーから入力を受け取る関数
eval(userContent); // 危険!ユーザーが入力した内容がそのままコードとして実行される可能性があります。
もしユーザーが "alert('ハッキングされました!')" といった文字列を入力したら、そのスクリプトがそのまま実行されて予期せぬ動作を引き起こします。これはウェブサイトの重要情報漏洩やサービス障害につながる可能性のある重大なセキュリティ脅威です。
2. パフォーマンス低下
eval() は JavaScript エンジンのコード最適化を妨げます。最新の JavaScript エンジン(JIT コンパイラなど)は、コードを実行する前に解析して最適化するプロセスを経ています。しかし eval() が使われると、エンジンは eval() 内のコードがどの変数を参照・修正するのか予測できなくなります。
結果として、エンジンは変数名の参照などを遅い方式で処理せざるを得ず、場合によってはコンパイルされたコードを再び解釈しなければならないかもしれません。これはアプリケーション全体のパフォーマンス低下につながります。
より安全な代替:new Function()
eval() のリスクを避けつつコードを動的に実行できる、より良い方法があります。つまり、new Function() コンストラクタを使うことです。
new Function() はパラメータリストと関数本体を文字列として受け取り、新しい関数オブジェクトを生成して返します。
new Function() の使い方
基本的な文法は以下の通りです。
let func = new Function ([arg1, arg2, ...argN], functionBody);
例えば、2 つの数値を足し合わせる関数を new Function() で作成してみましょう。
const add = new Function('a', 'b', 'return a + b');
console.log(add(2, 3)); // 出力:5
このように new Function() を使えば、文字列で定義されたロジックを持つ関数を動的に生成できます。
new Function() はなぜより安全なのでしょう?
new Function() と eval() の最も重要な違いは 実行コンテキスト(Execution Context) にあります。
1. 実行コンテキストの違い
-
eval():現在実行中の ローカルスコープ(Local Scope) でコードを実行します。つまり、eval()が呼び出された関数内のローカル変数にアクセス・修正が可能です。 -
new Function():常に グローバルスコープ(Global Scope) で実行される関数を生成します。new Function()で作られた関数は、それが作られた時点のローカルスコープにアクセスできません。
2. スコープの制限によるセキュリティ強化
new Function() で生成された関数はクロージャー(closure)を形成せず、外部のレキシカル環境(Lexical Environment)を参照しません。参照できるのはグローバルスコープのみです。
次の例で違いを明確に確認してみましょう。
function demo() {
let localVariable = '私はローカル変数です。';
// eval() はローカルスコープにアクセス可能
eval("console.log(localVariable);"); // 出力: "私はローカル変数です。"
// new Function() はローカルスコープにアクセス不可
try {
const myFunction = new Function("console.log(localVariable);");
myFunction();
} catch (e) {
console.error(e); // 出力: ReferenceError: localVariable is not defined
}
}
demo();
上記のコードから分かるように、eval() は demo 関数のローカル変数である localVariable にアクセスできますが、new Function() で生成された関数はアクセスできず、ReferenceError が発生します。このように new Function() は外部変数へのアクセスを根本的に遮断できるため、悪意あるコードがローカル変数経由でシステムに影響を与えるのを防ぎます。
3. パフォーマンス面の利点
eval() が JavaScript エンジンの最適化を妨げる一方で、new Function() は比較的パフォーマンスに有利である可能性があります。new Function() で生成されたコードは別の関数本体の中に存在し、ローカルスコープを汚染しないため、エンジンがコードをより簡単に解析し最適化できるからです。
もちろん、new Function() もランタイムに文字列を解析・コンパイルするプロセスを必要とするため、静的に関数を宣言するよりは遅いですが、動的にコードを実行しなければならない状況では eval() よりもずっと良い選択肢です。
new Function() 使用時のベストプラクティス
new Function() は eval() より安全とはいえ、依然としてユーザー入力を直接コード本体に使用することは危険となり得ます。動的にコードを生成する際には、常に次の指針を守ることをお勧めします。
-
ユーザー入力を直接使わないこと:計算機ロジックを作るなら、ユーザーが入力した完全な数式をそのまま
new Function()に渡さないでください。代わりに、入力値を解析して安全が確認された演算子(+, –, *, /) と数字だけを組み合わせて関数本体を生成しましょう。 -
厳格モード(Strict Mode)を使用する:
new Function('"use strict"; ...')のように関数本体の開始部に'use strict'を追加して、より安全なコードを書くようにしましょう。厳格モードは、いくつかの危険な構文をエラーとして処理してくれます。 -
代替策をまず検討すること:動的にコードを生成する必要性について、もう一度考えてみてください。多くの場合、静的な関数やデータ構造(例:オブジェクトやマップ)を用いて、同じ機能をより安全・効率的に実装できます。
より良いコードに向けて
eval() は強力で便利な機能ですが、その裏には重大なセキュリティおよびパフォーマンスリスクが存在しています。ドキュメントでも eval() を「絶対に使用すべきではない(Never use eval!)」と強く警告しています。
幸いにも、私たちには new Function() という、より安全で効率的な代替手段があります。new Function() は実行スコープをグローバルに限定することで eval() の主要なセキュリティ脆弱性を解消し、コード最適化にも有利です。
もちろん最良の方法は、そもそも動的にコードを生成する状況を避けることです。しかしどうしても必要な場合には、これからは eval() の代わりに new Function() を使って、より安全で堅牢なコードを書いてみましょう。あなたのコードは一段とレベルアップするはずです。 :)