@ledsun blog

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

ruby.wasmでJavaScriptのオブジェクトをnewメソッドで初期化する

ruby.wasmでクエリ文字列を扱おうとしたら - @ledsun blog

JS.global.URLSearchParams.newと書きたい

と書きました。 ためしに実装してみました。

class JS::Object
  def method_missing(sym, *args, &block)
    if sym == :new
      # new で呼び出されたら、コンストラクタとして呼び出す。
      JS.eval("return #{self.to_construct(args)}")
    elsif self[sym].typeof == "function"
      # 関数として定義されていたら、関数として呼び出す。
      self.call(sym, *args, &block)
    else
      super
    end
  end

  # When call new, received self is like a 'function URLSearchParams() { [native code] }'
  # so, we need to convert it to 'new URLSearchParams()'
  def to_construct(args)
    "new #{constructor_name}(#{to_js_argument_string(args)})"
  end

  def constructor_name
    self.to_s.match(/function\s+([^(]+)/)[1].strip
  end

  # When call new, received argument is like a '["?phrase=%E3%81%82%E3%81%84%E3%81%86"]"
  # But converting string strips double quotes, so we need to add double quotes.
  def to_js_argument_string(args)
    "#{args.map do
      to_string_with_quote _1
    end.join(', ')}"
  end

  # Convert to string with double quotes.
  # Support Ruby String and JavaScript String both.
  def to_string_with_quote(var)
    var.is_a?(String) || var.typeof == "string" ? "\"#{var}\"" : var.to_s
  end
end

想像していたより大変でした。これで

JS.global[:URLSearchParams].new(JS.global[:location][:search])

と書けます。 実際にやっているのはJavaScript

eval('return new URLSearchParams("?phrase=あいうえお"')

を実行しています。 つまりJavaScriptコンストラクターを呼び出す文を作って実行しています。

つまづいた点

newメソッドのレシーバーは関数オブジェクト

selffunction URLSearchParams() { [native code] }のような関数オブジェクトです。 ここから関数の名称を正規表現で取得しています。 ユーザーが作成した関数や無名関数などこのパターンにあてはまらないコンストラクター関数もありそうに思います。 未確認です。

引数をJavaScriptの文字列に展開するときに引用符が消える

"return new URLSearchParams(#{args.join(", "))"

で引数を展開すると、引数の価が文字列だったときに"が消えてJavaScriptのパースエラーになります。 そこで引数の型が文字列のときだけでくくるto_string_with_quote関数を通しています。 またRubyの文字列とJavaScriptの文字列を両方判定するためにvar.is_a?(String) || var.typeof == "string"で判定しています。