使用lambda在Ruby裡模仿實現JS的物件及原型繼承

Cichol發表於2014-12-13

ruby沒有對原型模式的支援實在是有些遺憾,雖然Matz在書裡說可以通過object.clone實現原型,但這只是看上去像而已,實際上並不會形成一條原型鏈。

a.attr = 10
b = a.clone
a.attr = 9
b.attr             # => 10

clone可以使b有和a一樣的屬性,但b擁有的是屬性的拷貝,即使a的屬性改變了,也不會對b有影響,這不符合原型鏈的定義。按原型鏈繼承的物件,如果沒有某屬性,應當沿著原型鏈向上搜尋,找原型的屬性替代。

要在ruby實現原型鏈我首先想到是使用物件的單件類,但是單件類不允許new也不允許繼承,甚至連方法都不能解綁換綁到別的物件上。接著我考慮藉助ruby的類繼承,因為類繼承和原型繼承是非常相似的,然而很快遇到了問題,js裡原型是可以隨意更換的,而ruby的類在定義的時候就要確定繼承關係,之後無法更改父類,所以這個想法也不可行。

於是考慮自己實現繼承的功能,每個物件設定一個prototype屬性,當獲取屬性時,如果屬性在當前物件不存在,則會去prototype指向的物件內查詢,一層一層遞進,直到prototype為nil。

https://github.com/CicholGricenchos/tricks/blob/master/prototype/using_ruby_object.rb

藉助ruby本身的物件系統,很容易就實現了一個。其中物件的方法是用lambda儲存的,而lambda會在建立時和周圍的上下文繫結在一起,而給物件設定屬性的時候,這些lambda都是在物件外建立的。為了讓這些lambda能操作物件內部,必須重新設定上下文,這裡我使用了instance_eval,把繫結設在例項以內。

不過觀察js物件的函式,只會設定一個this指標,並不會改變上下文,這個ruby版本雖然功能大致實現了,但實現得還不好,不夠geek。接著我想能不能不借助Object,單純用閉包實現一個呢?

https://github.com/CicholGricenchos/tricks/blob/master/prototype/using_pure_lambda.rb

這是最終實現的程式碼,思路是用一個閉包表示一個物件,閉包內有個variables hash儲存物件的屬性,然後暴露一個lambda用於提供get set操作,也就是程式碼裡的handler。

這個handler可以接受兩個引數,屬性名稱和屬性值,如果只提供名稱,則為get操作,返回這個屬性的值,如果二者都提供,則為set,將屬性設為給定的值。

PS:lambda[*arg] 以及 lambda.(*arg)是呼叫的語法糖,等價於lambda.call(*arg)。

實踐的時候很快就遇到了麻煩,還是lambda上下文的問題,在物件外建立的lambda讀取不了物件的variables。我一開始想參照前一版的做法,把lambda的binding改到物件內,但發現不使用instance_eval之類是做不到的。嘗試了諸如重新建立lambda的辦法,都失敗了。

摸索的過程記在了 https://ruby-china.org/topics/23160

後來突然想到,如果不能轉換上下文,不如把需要用到的量通過eval插入到lambda的上下文裡,相當於構造一個上下文,也能達到目的。最直觀的想法就是讓lambda的上下文有個指向物件自身的this,這和js的做法也很相似,之後就可以通過this[:attr]來讀取自身的屬性。

一開始我是用 eval "this = #{物件名稱}", lambda.binding 的形式進行插入的,因為eval只能接收字串程式碼,所以我必須知道物件的名稱才能讓this指向它。但是物件可以隨時在不同名稱的變數中轉移,要記錄變數的名稱很不現實,如果可以把handle傳進lambda的binding就好了。

想了很久,跨作用域傳遞一個量看上去是不可能的,不過有些變數是每個域都可以訪問的,就是全域性變數,通過全域性變數可以讓兩個毫不相關的上下文進行通訊。於是我把handler放到一個全域性變數裡,再eval一個this方法(至於為什麼是方法,可以參照ruby-china那個帖子),指向這個全域性變數,問題就解決了。

後面我把這個流程封裝到pack_lambda裡,這個lambda的作用是在物件方法執行前插入this指標,執行方法,然後再置空this,返回方法的返回值。還順便提供了一個建構函式的機制。

現在物件的方法lambda和js函式的行為是一致的,擁有一個指向物件自身的this指標,所有操作都通過這個this完成,而上下文環境不會改變,同時很好地實現了原型繼承功能,目標達成了。

相關文章