◆ 文字列を eval したい
◆ 変数は eval スコープのローカルじゃなくてオブジェクトのプロパティに入ってる
◆ 変数(プロパティ)だけじゃなくて関数や演算子などの式もある

◆ eval でローカルスコープにもってくる
  ◆ できれば避けたいやり方
◆ Function 関数で eval して引数形式で渡す


ちゃんと書くと文字数がすごく長くなるのでわかりづらいタイトルになっててすみません
今回やりたいのは ユーザ入力の文字列を評価してその値を取得することです

式の文字列を評価するなら JavaScript では eval を使えば一発 なのですが そのままではだめな理由があります

式には

  • 100
  • fn("abc")
  • num1 + num2

のように普通の JavaScript の式が入ります

eval するスコープで使える変数は オブジェクト形式で渡すので そのまま eval しても参照できません

一番簡単な解決方法は with を使うことです

function evaluate(expression, values) {
with(values){
return eval(expression)
}
}

evaluate("num + 1", {num: 100})
// 101

with は使うべきじゃないと言われていますが 考え無しであちこちで使うようなことをしなければ便利な機能ですし with が最適ってシーンでは避ける必要はないと思います
グローバルスコープの window 自体が with されてるようなものですし JavaScript とは深い関係な機能ですし


……ですが strict モードでは with を使うことはできません

strict モード自体 必須な機能でもなく 使うことのメリット・デメリットがあるので 私は必要なときだけしか strict モードは使いませんし strict 必須にすべきとは思いません

それなのに module では強制 strict となります
余計なことしなくていいのに と思いますが module で使うなら仕方ないのでなんとか with を使わずに実装しないといけません

eval が増える

とは言っても with なしで 変数を参照する場合のみ オブジェクトのプロパティを参照させるなんてできないです
式じゃなくて変数名と決まってるなら values[name] 形式にできますが 1 + 1 などが来ると機能しなくなります

式をパースして変数名だけを取り出して特別に処理 なんてことは絶対に避けたいです
四則演算くらいのシンプルさならともかく 関数呼び出しや無名関数作成 テンプレートストリング など JavaScript の構文すべてをサポートして変数名だけ取り出すなんてそう簡単にはできません

eslint とか tern とか flow とかで使われてるライブラリを持ってくれば可能かもしれませんがそんな大事にはしたくないです


他に考えられるのが あまり気の進まない方法ですが オブジェクトの中身全部をローカルスコープに持ってくること
動的に var name = value というコードを実行してローカル変数を定義します
パフォーマンスとかわかりやすさとかいろいろな部分でイヤなコードですがシンプルにかける点では一番です

function evaluate(expression, values) {
for(const key of Object.keys(values)){
isValidVariableName(key) && eval(`var ${key} = values["${key}"]`)
}
return eval(expression)
}

evaluate("num + 1", {num: 100})
// 101

isValidVariableName は省略してますが 変数名として有効化チェックする関数です
正確にチェックするのは結構メンドウなので try-catch で宣言できなければ無視するのでもいいかなと思います
それ以前に関数に渡す引数に変数名に使えないものなんて入れるはずないので 使う側の問題ということでチェックすらなくてもいいかも

キーに変な文字列いれてインジェクション攻撃っぽいこともできなくはないですけど 普通は関数使う人がそんなの渡さないですし そんなことするなら直接 JavaScript で直接書いてしまえばいいだけですからね

const と var を使い分けてるのは for 内で消えてほしい変数と eval のスコープまで残っていてほしい変数を分けるためです

結構いい感じな解決策

作ってみては見たものの 見直すたびになんかイヤだ
もっと気持ち良いコードで書きたい 何か方法ないの?とあれこれ考えてました


そんなとき ローカルに展開するのは eval よりもっといい方法がある と気づきました
引数に渡せばいいんです
eval 以外にも Function 関数を使って文字列を評価させることができます

function evaluate(expression, values){
return Function(...Object.keys(values), `return ${expression}`)(...Object.values(values))
}

evaluate("num + 1", {num: 100})
// 101

evaluate("b(a + c)", {a:1, b:function(x){return `{${x}}`}, c:"a"})
// 1a

「return 」 と書いてる部分だけかっこよくないですが 全体的にはいい感じです
eval を使わずひとつの式でできてるのもいいところです