この記事はOpal Advent Calendar 2016の24日目の投稿として書いています。
今日はクリスマスイブですね。昨日、予告したとおり今日はJSの実装について書きます。
例によって使いかたからです。
require 'js'
%x{
function Foo() {
this.bar = function() {
return 'bar';
};
this.baz = 'baz';
};
}
foo = JS.new(`Foo`)
puts foo.JS.bar
puts foo.JS[:baz]
昨日のサンプルと似ていますね。どちらもJavaScriptのコードを呼びだすためのAPIということがわかります。
Native
はRubyのオブジェクトでラップしてRubyのオブジェクトとして扱えるようにしたものですが、JS
はJavaScriptのオブジェクトを直接操作するためにつくられています。
foo = JS.new(`Foo`)
この呼び出しで返ってくるオブジェクト(foo
)はRubyのオブジェクトではなく、JavaScript側のオブジェクトとして返ってきます。
つまり
foo = `new Foo()`
と書くのと等価です。(後で見ますが実際にはapply
をつかって呼び出しています。)
foo
はJavaScriptのオブジェクトなので、Rubyのメソッドとしてbar
を呼ぶことができません。
(Native
ではmethod_missing
を利用してbar
を呼ぶとJavaScriptのbar()
関数が呼び出されました。)
そこで、RubyのなかでJavaScriptのbar()
関数を呼ぶには、
foo.JS.bar
のようにJS
というメソッド(?)を介して呼びだします。
このコードは
`foo.bar()`
と等価になります。
それでは、stdlib/js.rbのコードを見てみましょう。
new
の実装はこのようになっています。
if `typeof Function.prototype.bind == 'function'`
def new(func, *args, &block)
args.insert(0, `this`)
args << block if block
`new (#{func}.bind.apply(#{func}, #{args}))()`
end
else
def new(func, *args, &block)
args << block if block
f = `function(){return func.apply(this, args)}`
f.JS[:prototype] = func.JS[:prototype]
`new f()`
end
end
bind
が定義されているかどうかで分岐していますが、基本的にはコンストラクタをapply
してnew
するという感じです。
new
の他にも、delete
,in
,instanceof
,typeof
が定義されていますが、単純にJavaScriptのコードを呼ぶだけになっています。
また、call
というメソッドも用意されていて、グローバルな関数を呼び出すことができます。
def call(func, *args, &block)
g = global
args << block if block
g.JS[func].JS.apply(g, args)
end
alias method_missing call
これはmethod_missing
にaliasされているので、明示的にcall
しなくても、JS.parseInt()
のように呼びだすこともできます。
このようなJS
モジュールに定義されるメソッドはstdlib/js.rbに定義されていますが、foo.JS.bar
のようなものはどのように実装されているのでしょうか。
実はstdlib/js.rbにはこの実装はありません。ためしにrequire 'js'
をしなくてもこれらのコードはちゃんと実行できます。
ではどこにあるのでしょうか?
先程、
foo.JS.bar
は
`foo.bar()`
と等価と書きましたが、実際にコンパイルされたコードをみるとまったく同じにコンパイルされています。
ということはJS
というのはどこに行ってしまったのでしょう?
答えはOpal::Rewriters::DotJsSyntax
というクラスにあります。(lib/opal/rewriters/dot_js_syntax.rb)
def on_send(node)
recv, meth, *args = *node
if recv && recv.type == :send
recv_of_recv, meth_of_recv, _ = *recv
if meth_of_recv == :JS
case meth
when :[]
if args.size != 1
raise SyntaxError, ".JS[:property] syntax supports only one argument"
end
property = args.first
node = to_js_attr_call(recv_of_recv, property)
when :[]=
if args.size != 2
raise SyntaxError, '.JS[:property]= syntax supports only two arguments'
end
property, value = *args
node = to_js_attr_assign_call(recv_of_recv, property, value)
else
node = to_native_js_call(recv_of_recv, meth, args)
end
super(node)
else
super
end
else
super
end
end
Opalのコンパイラはrecv.JS
というコードに対して書き替えをおこなうようです。JS
の後に続く文字列によって、プロパティへのアクセスあるいはアサイン、メソッドコールの3パターンに分れます。
それぞれ、to_js_attr_call
,to_js_attr_assign_call
,to_native_js_call
を呼びだしています。
たとえば、to_js_attr_call
だったら次のように定義されています。
def to_js_attr_call(recv, property)
s(:jsattr, recv, property)
end
Opalのパーサーはs式を模した構文木を作るので、s
というメソッドの呼びだしになっています。
ここでは、:jsattr
というノードをつくります。
このノードがコードを生成するのは、lib/opal/nodes/call_special.rbに定義されています。
# recvr.JS[:prop]
# => recvr.prop
class JsAttrNode < Base
handle :jsattr
children :recvr, :property
def compile
push recv(recvr), '[', expr(property), ']'
end
end
JS
という一見同じ機能に見えたものは二つの機能によって実現されていました。
-
JS
モジュールによる、JavaScriptのグローバルな関数や演算子の利用 -
recv.JS
のようなメソッド呼び出しのnativeコードへの書きかえ
この記事を書きはじめたときにはイブだったのですが、とうとう日付が変ってしまいました。
メリークリスマス!!!