本文分上、下兩篇,站在一個(gè)難以名狀的角度上研究了 JavaScript 語言中面向?qū)ο髾C(jī)制的起源、內(nèi)涵和發(fā)展,帶領(lǐng)讀者從原始森林走向高樓大廈。文章作者 lichray 是個(gè) ECMAScript 的狂熱追隨者,mozilla.org 郵件列表里的無名潛水員。 文章中使用了 Rhino 解釋器,行開頭有 "js>" 表示那是輸入,輸入下一行沒有這個(gè)標(biāo)記的表示解釋器回饋消息。 PS: 讀懂本文需要對 JavaScript 閉包和逃逸變量有較深入的了解。
一. 對象和消息 考慮一下我們平常怎么說話的。我們叫某某人做某事,用下面的句式: forest run! 其中"!"是語氣的標(biāo)志,對于編程語言來說是沒有意義的,全部換成".": forrest run. 不知道如果我告訴大家上面這句話就是 Smalltalk 語言中一個(gè)合法語句大家會怎么想。好了,不談這個(gè)。這樣我們就得到了一種語法,"賓"謂結(jié)構(gòu): ObjectVerb :: Object Verb. 如果讓它支持多個(gè) Verb,比如 forrest run, jump, stop. 可以擴(kuò)展成這樣: ObjectVerb :: Object VerbList. VerbList :: Verb Verb , VerbList 很明顯,對于 JavaScript 來說,上面的 BNF 不可能和任何一個(gè)產(chǎn)生式匹配。問題出在哪兒?我們要幫 JavaScript 指定,誰是 Object,誰是 Verb。鑒于 Object 只有一個(gè),Verb 有多個(gè),我們可以用括號來區(qū)分它們,然后把最后那個(gè)句號去掉: ObjectVerb :: Object ( VerbList ) 這樣上面的那句話就變成了下面的形式: forrest (run, jump, stop) 很像函數(shù)調(diào)用,是吧?不過還有一個(gè)問題,現(xiàn)在這些 Verb(s) 對于 JavaScript 來說是“裸詞”(Perl 語),我們可以避開再去定義這些標(biāo)識符,用字符串代替;最后再說明一下 Object 是什么: forrest ('run', 'jump', 'stop') 那么現(xiàn)在我們第一個(gè)“模仿”自然語言的程序版本出現(xiàn)了,加上下面針對 JavaScript 的文法: Object :: Identifier Verb :: StringLiteral
二. 實(shí)現(xiàn)消息傳遞 有了文法,一切都好辦?吹贸鰜恚覀兿旅娴墓ぷ魇嵌x能創(chuàng)建一個(gè)新 Object 的函數(shù),函數(shù)中有一些動(dòng)作,產(chǎn)生的新 Object 是一個(gè)能處理這些消息的函數(shù)。創(chuàng)建 Forrest Gump 的函數(shù)還可以創(chuàng)建 Tom,Mike 等等;他們都是 People: function People () { function run () { print("I'm running!") } function jump () { print("I'm jumping!") } function stop () { print("I can't stop!") } return (function (verb) { switch (verb) { case 'run': run(); break case 'jump': jump() ;break case 'stop': stop() ;break } }) } 為了簡單起見還可以把返回的那個(gè)函數(shù)寫成這樣: (function (verb) { eval(verb)(); } }) Ok。現(xiàn)在我們來試一試這個(gè)智商低于 85 的 Forrest Gump 怎么樣: js> forrest = People() js> forrest('run') I'm running! js> forrest('jump') I'm jumping! js> forrest('stop') I can't stop! 事情就是這樣。我們成功地創(chuàng)造了對象,還讓他做動(dòng)作、說話。 不過,這個(gè)實(shí)現(xiàn)并不是我們上文中最后一個(gè)文法所指出的。它不支持連續(xù)發(fā)送指令。改一改。要加入順序執(zhí)行指令的辦法: function People () { function run () { print("I'm running!") } function jump () { print("I'm jumping!") } function stop () { print("I can't stop!") } function _do_verbs_ (verblist) { for (var i=0; i < verblist.length; i++) eval(verblist[i]).call() } return (function () { _do_verbs_(arguments) }) } 這下似乎比較像樣了: js> forrest = People() js> forrest('jump','run','jump','stop') I'm jumping! I'm running! I'm jumping! I can't stop!
三. 利用消息傳遞處理狀態(tài) 什么是狀態(tài)?我們在進(jìn)行面向?qū)ο缶幊虝r(shí),把狀態(tài)表示為對象的一組數(shù)據(jù),我們稱之為“屬性(property)”。在我們的消息傳遞編程風(fēng)格中,可以直接把這些數(shù)據(jù)堆到產(chǎn)生對象的那個(gè)函數(shù)中去。下面給 Forrest 加入一個(gè)狀態(tài),F(xiàn)orrest 口袋里的錢。先得聲明原先有多少錢: forrest = People(1000) 然后,我們希望可以執(zhí)行這樣的代碼,讓 forrest 支出 200 美元: forrest('pay', 200) 但很明顯,我們無法分清 200 是 Verb 還是 'pay' 所要求的數(shù)據(jù)。我們只得簡化文法,只允許一次發(fā)送一個(gè)消息,以保全我們的腦細(xì)胞: forrest('pay')(200) 也就是說,我們需要讓 forrest('pay') 這一表達(dá)式返回一個(gè)能改變狀態(tài)的函數(shù),而不僅僅是調(diào)用函數(shù)來顯示一句話。也就是說,如果我們想讓 Forrest 急得跳起來,我們先得跳起來: forrest('jump')() 新時(shí)代的 Forrest 實(shí)現(xiàn)如下(省略了一點(diǎn)多余的代碼): function People (money) { //var money = money function pay (dollars) { money -= dollars } function restMoney () { return money } function run () { print("I'm running!") } return (function (verb) { return eval(verb) }) } 試一下。先支出 200 美元,然后看看他還剩多少錢: js> forrest=People(1000) js> forrest('restMoney')() 1000 js> forrest('pay')(200) js> forrest('restMoney')() 800 當(dāng)然,我們的 Forrest 還可以賺錢。下面這個(gè)版本比較徹底地說明了消息傳遞編程風(fēng)格的一切?梢灾苯有薷腻X之后,我們可以不需要在創(chuàng)建 Object 的時(shí)候就說明原有多少錢;當(dāng)然,使用注釋中的版本更自然: function People (/* money */) { var money = 0; // var money = money ? money : 0; function setMoney (dollars) { money = dollars } function addMoney (dollars) { money += dollars } function pay (dollars) { money -= dollars } function restMoney () { return money } return (function (verb) { return eval(verb) }) } 試一下吧: js> forrest = People() js> forrest('addMoney')(1000) js> forrest('restMoney')() 1000 js> forrest('pay')(200) js> forrest('restMoney')() 800
上篇完。小結(jié)一下:消息傳遞的編程風(fēng)格指的是,把函數(shù) A 的執(zhí)行上下文當(dāng)作對象的數(shù)據(jù)環(huán)境,在此定義對象的動(dòng)詞(函數(shù)),然后從此上下文中返回一個(gè)可以接受、處理消息的函數(shù)(常為匿名)。用函數(shù) A 產(chǎn)生消息處理器作為對象,向此對象傳遞參數(shù)作為消息,以此執(zhí)行函數(shù) A 環(huán)境中定義的動(dòng)作,這些動(dòng)作還可能改變所在上下文中用一組數(shù)據(jù)定義的對象狀態(tài)。
這是最終確定的 JavaScript 基于消息傳遞編程風(fēng)格的文章“OOP 詭異教程(上)”的下篇。原來的想法是以風(fēng)格開頭,談到 JavaScript 的內(nèi)部機(jī)制,但作者 lichray 遲遲沒有動(dòng)鍵盤,認(rèn)為不如利用已有的風(fēng)格做一套機(jī)制出來,這樣可能更有意義。于是,就有了這個(gè)更加“詭異”的下篇。
這篇文章的宗旨是利用我們僅有的“賓謂”語法構(gòu)造出完整的一套面向?qū)ο髾C(jī)制,所以更多代碼在更多的時(shí)候是不應(yīng)在實(shí)際工作中使用的(也算一種元語言抽象),所以類似效率、代碼風(fēng)格之類的問題反對回帖質(zhì)疑。
四. 擴(kuò)展的實(shí)現(xiàn) 上文最后給出了一個(gè)“看上去很美”的基于消息傳遞的編程風(fēng)格,比如構(gòu)造一個(gè) People 類的代碼類似:
function People () { var money = 0 function setMoney (dollars) { money = dollars } function pay (dollars) { money -= dollars } return (function (verb) { return eval(verb) }) }
有了這樣的語法我們就可以描述不少句子了。但是存在一個(gè)問題:現(xiàn)實(shí)中的 Objects 之間是存在關(guān)系的——比如,forrest 是個(gè) IQ 為 75 的傻子,傻子是 People 的一種。而我們僅僅是生搬硬套了一種語法而割裂了這種 "is-a" 關(guān)系。現(xiàn)在我們的工作,目的之一就是讓這樣一個(gè)“真切”的世界從我們已有的編程風(fēng)格的地基上拔地而起。 到底應(yīng)該怎樣做才能使 Fool 產(chǎn)生的對象都能響應(yīng) People 的消息呢?我們要給 Fool 產(chǎn)生的對象(也就是返回的那個(gè)匿名函數(shù)啦)都添加這樣一種能力:如果在 Fool 中響應(yīng)不了消息,那就反饋給 People 響應(yīng)。
function Fool (iq) { var IQ = iq || 0 function init (iq) { IQ = iq } return (function (verb) { try { return eval(verb) } catch (e) { return People()(verb) } }) }
js> forrest = Fool() js> forrest('init')(75) js> forrest('IQ') 75 js> forrest('money') 0
五. 語法擴(kuò)展和代碼生成 這下代碼量增加了很多,強(qiáng)迫潛在的使用者們在創(chuàng)建每個(gè)類時(shí)都這樣寫那實(shí)在是令人抓狂。本來這篇文章應(yīng)該不提此類問題的解決,但考慮到有益于讀者理解“機(jī)制”這個(gè)抽象概念,這里給出一個(gè)可行的方案——把普通的類代碼用 Function() 函數(shù)重編譯為可用的 JavaScript 函數(shù)。也就是說,我們能給出類擴(kuò)展的代碼并指定被擴(kuò)展的類來獲取類似上文的代碼:
Fool = extend('People()', function (iq){ var IQ = iq || 0 function init (iq) { IQ = iq } })
為了方便字符串操作,我們希望編譯后的代碼的參數(shù)部分(如 People())都集中出現(xiàn)在一個(gè)位置且盡可能便于定位。在函數(shù)頭添加一句
var origin = People()
當(dāng)然是可行的,這樣還能使 Fool 內(nèi)部顯式引用到其超類。但這樣還不夠漂亮。我們修改編譯后的樣例代碼為:
function () { return (function (origin) { var IQ = 0 function init (iq) { IQ = iq } return (function (verb) { try { return eval(verb) } catch (e) { return origin(verb) } }) })(People()) }
這個(gè)利用參數(shù)傳遞變量的小技巧不值得學(xué)習(xí),實(shí)際效率不高。但在這篇文章中,這樣綁定特殊變量的技術(shù)是標(biāo)準(zhǔn)方案。 那么,extend() 函數(shù)的實(shí)現(xiàn)為:
function extend (originc, code) { function argsArea (code) { // 題外話,正則表達(dá)式也有不值得使用的時(shí)候 return code.slice(code.indexOf('(')+1, code.indexOf(')')) } function bodyCode (code) { // 不用 trim() 了,沒事兒找事兒 return code.slice(code.indexOf('{')+1, code.lastIndexOf('}')) } function format (body) { var objc = bodyCode(function () { return (function (verb) { try { return eval(verb) } catch (e) { return origin(verb) } }) }.toString()) return 'return (function (origin) {'+body+objc+'})('+originc+')' } var $ = code.toString() return Function(argsArea($), format(bodyCode($))) }
這樣前文提到過的 extend 的實(shí)例代碼就可以正常運(yùn)行了,測試代碼不再重復(fù)。
六. 機(jī)制完備化 這樣,我們的基于消息傳遞編程風(fēng)格的一套面向?qū)ο髾C(jī)制就確定下來了。機(jī)制是憲法,是語言的根本***,有了它,我們就可以通過修改代碼生成器,很快地給這套機(jī)制進(jìn)行完備化。 想法有很多,例子只舉兩個(gè)。 第一個(gè)例子:類的定義中應(yīng)該能直接引用到將產(chǎn)生的對象 self。答案只有一句話:把返回的那個(gè)作為對象的匿名函數(shù)命名為 self。 第二個(gè)例子:既然是單繼承模式,應(yīng)當(dāng)存在一個(gè)頂層類 AbsObj,使沒有指定繼承的類自動(dòng)繼承它。答案也只有一句話:在 extend 函數(shù)體第一行添加代碼:
if (arguments.length == 1) { code = originc originc = 'AbsObj()' }
然后手工構(gòu)造設(shè)計(jì) AbsObj 類,為空也無所謂。不過當(dāng)然了,一般都會給頂層類添加一些全局性質(zhì)的消息綁定。由于是“底層操作”,基本上都需要修改 extend 函數(shù)。做了一個(gè)簡單的:
function AbsObj () { //檢測是否能響應(yīng)此 verb,要再用一次異常處理 function canHandle(verb){ try { // 別擔(dān)心這里的 self 會傳遞不過去 self(verb) } catch (e) { return false } return true } function toString() {} // 這個(gè)搞起來其實(shí)很麻煩~` var self = function (verb) { return eval(verb) } return self }
js> Obj=extend(function(){x=5}) js> o=Obj() js> o('canHandle')('x') true js> o('canHandle')('y') false
文章寫完了,小結(jié)一下。消息傳遞的編程不僅僅是一種代碼風(fēng)格,還可以成長為一種完備的機(jī)制。這種完備性遠(yuǎn)不只是這兩篇加起來不到300行的文章所能覆蓋的(例如非常徹底的“萬物皆對象”,因?yàn)橹灰悄茼憫?yīng)消息的函數(shù),連接一下 AbsObj 就是合法對象了;類,函數(shù)都可以),大家可以試著玩一玩,順便體會一下這個(gè)計(jì)算模型的透明和強(qiáng)大。 另外,熟悉函數(shù)式編程的朋友可以幫忙思考一下:這樣一個(gè)基于閉包變換的計(jì)算模型實(shí)質(zhì)上是函數(shù)式的,再配合動(dòng)態(tài)的函數(shù)式的對象級繼承(用一個(gè)匿名類代換一下)就能在純 FP 真正下實(shí)現(xiàn) OOP 了。可惜的是每一次更新操作都要重新生成對象,性能代價(jià)大了點(diǎn),不知道大家有什么好想法。
|