JSでクラスを文字列から動的生成する3つの方法
JSでクラスを文字列から動的生成する3つの方法
eval()
,Function()
,import()
。
成果物
方法
eval()
Function()
import()
コード
eval()
function getClass1(className) { return eval(className); }
Function()
function getClass2(className){ return Function(`return (${className})`)(); }
import()
async function getClass3(className) { const module = await import(`./parser/${className}.js`); return module.default; }
使い分け
ES Moduleを使うならimport()
、使わないならFunction()
で実装する。eval()
はどちらでも使える。
コード | ES Module |
---|---|
import() |
使う |
Function() |
使わない |
eval() |
使う/使わない |
問題
コード | 問題 |
---|---|
eval() |
セキュリティ問題あり(インジェクション攻撃を受けうる)。ES Module仕様時にエラーで使えない。 |
Function() |
ES Module仕様時にエラーで使えない。 |
import() |
パス指定が必要(呼出ファイルからの相対パスで指定せねばならない)。Promise (またはasync ,await )の形で受け取って実装せねばならない。 |
これからの時代はimport()
を使うべき。だが一気にコードが面倒になる。また、webpackでのビルドができないなどの問題にぶつかる。実行環境が少なくなってしまう。
共通化
ES Moduleを使う/使わないによってコードを使い分けねばならない。そんなの嫌だ。なんとか共通化できないか。
eval()
は共通化できる
さすがeval()
先生。無敵。なんでもできる。セキュリティ? 知らんな。かまわず実行しちまうんだぜ。
import()
は共通化できない
import()
はES Moduleを使うときでないと使えない。undefined
が返される。new
するときに型エラーで怒られる。
Uncaught (in promise) TypeError: cls3 is not a constructor
Function()
は共通化できない
import()
はES Moduleを使うときでないと使えない。ならFunction()
はES Moduleを使う/使わないに限らずどちらの場面でも使えるのでは? と思うだろうが、使えない。
Function()
はES Moduleのとき未定義エラーになる。動的生成したいクラスが未定義であると怒られる。
Uncaught (in promise) ReferenceError: Human is not defined
コスト
どの方法を使うのが一番楽か。ES Moduleを使わずFunction
にするのが一番楽なのでは?
コード | インジェクション攻撃対策 | ES Module | HTML<script> |
export ,import |
default |
Promise(async,await) |
---|---|---|---|---|---|---|
import() |
不 | 使う | 1つ(type="module" ) |
全クラス | 要判断 | 要判断 |
Function() |
不 | 使わない | 全部網羅 | 不要 | 不要 | 不要 |
eval() |
必 | どちらでも | どちらでも | どちらでも | どちらでも | どちらでも |
コストはファイル数に依存する。ここでは5
ファイルと仮定する。たとえばESModuleを使わないときはファイル数が多いほどHTMLで<script>
を網羅するコストが高い。依存順を考えねばならないのもコスト増大の要因である。かといってESModuleを使ったときもコストが跳ね上がる。全クラスファイルに対してexport
,import
,default
,Promise(async,await)
を適切に判断して実装せねばならないから。
インジェクション攻撃対策のコストはファイル数に依存しない。どこまで許すか。どう実装するか。それによってコストが大きく変わる。それを考えねばならないだけでも多大なコスト&リスクである。固定値にはできないため5
と仮定する。
eval()
以外はESModuleを使う/使わないどちらか一方のときのみしか使えない。もし変更になったら多大な修正をせねばならない。そのリスクをコスト3
と仮定する。
項目 | コスト |
---|---|
ES Module使う/使わない変更可能性 | 3 |
インジェクション攻撃対策 | 5 |
HTML<script> |
5 |
export ,import |
5 |
default |
5 |
Promise(async,await) |
5 |
コード | インジェクション攻撃対策 | ES Module | HTML<script> |
export ,import |
default |
Promise(async,await) |
---|---|---|---|---|---|---|
import() |
0 | 使う 3 | 1 | 5 | 5 | 5 |
Function() |
0 | 使わない 3 | 5 | 0 | 0 | 0 |
eval() |
5 | 使う 0 | 1 | 5 | 5 | 5 |
eval() |
5 | 使わない 0 | 5 | 0 | 0 | 0 |
コード | コスト |
---|---|
import() |
19 |
Function() |
8 |
eval() ESM |
21 |
eval() 非ESM |
10 |
ランキングは以下。
ESM | コード | コスト |
---|---|---|
☓ | Function() |
8 |
☓ | eval() |
10 |
○ | import() |
19 |
○ | eval() |
21 |
ESModuleを使わないFunction()
がもっとも低コスト。
さらにここでは考慮していないが、ESMを使うと実行環境も限定されてしまう。ESMは開発コストだけでなく再現性が低いというリスクがある。よって開発・実行どちらの面をとってもESMを使わないほうがよいという結論になってしまった。
import
,export
とかイチイチ書かなくてもコードから自動で解析してくれたら違っただろうに。あと、import()
を使ったら必然的にPromise(async,await)
を使わねばならないというのもキツイ。async
,await
に至っては芋づる式に呼び出し元までそれを付与せねばならないから辛すぎる。
対象環境
- Raspbierry pi 4 Model B
- Raspberry Pi OS buster 10.0 2020-08-20 ※
- bash 5.0.3(1)-release
$ uname -a Linux raspberrypi 5.4.83-v7l+ #1379 SMP Mon Dec 14 13:11:54 GMT 2020 armv7l GNU/Linux