@ledsun blog

無味の味は佳境に入らざればすなわち知れず

ブラウザでruby.wasmのJS.evalしたときのスコープはなんなのか?

事の発端

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の定義です。

var - JavaScript | MDN

厳格モードでない場合は、スコープチェインで宣言されている同名の変数がない場合は、グローバルオブジェクト上にその名前のプロパティを作成しようとしていると仮定して、非修飾識別子に代入することになります。

この説明も難しいです。 が、要するにA = 1235window.A = 1235と解釈されます。

Bはローカル変数

class B { }はクラス宣言です

class - JavaScript | MDN

クラス式と同様、クラス宣言の内部は厳格モードで実行されます。

こちらはグローバルスコープでは宣言されません。

つまり

Aは下段の JS.eval から参照できますが、Bはできません。

JS.eval のスコープは?

ということは、JS.eval はグローバルスコープではなくて、なんらかのスコープを持っています。 もしスコープをもっていなければ、Bもグローバル変数として宣言されます。 下段の JS.eval から参照できるはずです。

ruby.wasmのJS.evalソースコードを見てみましょう。

https://github.com/ruby/ruby.wasm/blob/8e3731b3bf901c9c05a16ce385d93c6a61ec3dbe/packages/npm-packages/ruby-wasm-wasi/src/vm.ts#L232-L234

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 になる

(´・∀・`)ヘー

RubyJavaScriptの狭間の世界、楽しいです。

*1:雑にグローバル変数と呼んでいますが、より厳密にはwindowオブジェクトのプロパティです。