事の発端
tmtms さんが ruby-jp slack に面白いコードを投稿していました。
JS.eval <<~EOS A = 123 class B { } console.log(A) //=> 123 console.log(B) //=> class B {} EOS begin JS.eval <<~EOS console.log(A) //=> 123 console.log(B) EOS rescue => e p e #=> #<JS::Error: ReferenceError: B is not defined> end
上段の JS.eval
で定義されたAは下段の JS.eval
から参照できます。
ところがBはできません。
このコードの面白い点は
A = 123 class B { }
この辺りがすごくRubyっぽくて、JavaScriptだってわかっていても、脳が勝手にRubyと解釈してしまうところです。 Rubyだと、AとBは、どちらもグローバルスコープに宣言されます。
コードの動き
Aはグローバル変数
冷静にJavaScript脳で解釈するとA = 1235
はグローバル変数*1の定義です。
厳格モードでない場合は、スコープチェインで宣言されている同名の変数がない場合は、グローバルオブジェクト上にその名前のプロパティを作成しようとしていると仮定して、非修飾識別子に代入することになります。
この説明も難しいです。
が、要するにA = 1235
はwindow.A = 1235
と解釈されます。
Bはローカル変数
class B { }
はクラス宣言です
クラス式と同様、クラス宣言の内部は厳格モードで実行されます。
こちらはグローバルスコープでは宣言されません。
つまり
Aは下段の JS.eval
から参照できますが、Bはできません。
JS.eval
のスコープは?
ということは、JS.eval
はグローバルスコープではなくて、なんらかのスコープを持っています。
もしスコープをもっていなければ、Bもグローバル変数として宣言されます。
下段の JS.eval
から参照できるはずです。
ruby.wasmのJS.eval
のソースコードを見てみましょう。
evalJs: wrapTry((code) => { return toJSAbiValue(Function(code)()); }),
アロー関数のなかで、Function関数で関数を作成し実行しています。
アロー関数はスコープをつくります。
JS.eval
は、グローバルスコープではなく、この関数のスコープで実行されます。
その結果、クラス宣言で作成されたローカル変数Bは、他のJS.eval
から参照できません。
他のJS.eval
から参照したければ、window.B = class {}
のようにグローバル変数として宣言します。
おまけ
なぜJS.eval
は、eval()
関数ではなく、Function
関数を使うのでしょうか?
tompngさんに教えてもらいました。
function evalJS(code){eval(code)} evalJS('console.log(code)') // => "console.log(code)"
evalを実行するコンテクストのローカル変数(ここではcode)が参照できてしまいます。
function evalJS(code){Function(code)()} evalJS('console.log(code)') // => 正しく ReferenceError: code is not defined になる
(´・∀・`)ヘー
RubyとJavaScriptの狭間の世界、楽しいです。