本文分上、下两篇,站在一个难以名状的角度上研究了 JavaScript 语言中面向对象机制的起源、内涵和发展,带领读者从原始森林走向高楼大厦。文章作者 lichray 是个 ECMAScript 的狂热追随者,mozilla.org 邮件列表里的无名潜水员。 文章中使用了 Rhino 解释器,行开头有 "js>" 表示那是输入,输入下一行没有这个标记的表示解释器回馈消息。 PS: 读懂本文需要对 JavaScript 闭包和逃逸变量有较深入的了解。
一. 对象和消息 考虑一下我们平常怎么说话的。我们叫某某人做某事,用下面的句式: forest run! 其中"!"是语气的标志,对于编程语言来说是没有意义的,全部换成".": forrest run. 不知道如果我告诉大家上面这句话就是 Smalltalk 语言中一个合法语句大家会怎么想。好了,不谈这个。这样我们就得到了一种语法,"宾"谓结构: ObjectVerb :: Object Verb. 如果让它支持多个 Verb,比如 forrest run, jump, stop. 可以扩展成这样: ObjectVerb :: Object VerbList. VerbList :: Verb Verb , VerbList 很明显,对于 JavaScript 来说,上面的 BNF 不可能和任何一个产生式匹配。问题出在哪儿?我们要帮 JavaScript 指定,谁是 Object,谁是 Verb。鉴于 Object 只有一个,Verb 有多个,我们可以用括号来区分它们,然后把最后那个句号去掉: ObjectVerb :: Object ( VerbList ) 这样上面的那句话就变成了下面的形式: forrest (run, jump, stop) 很像函数调用,是吧?不过还有一个问题,现在这些 Verb(s) 对于 JavaScript 来说是“裸词”(Perl 语),我们可以避开再去定义这些标识符,用字符串代替;最后再说明一下 Object 是什么: forrest ('run', 'jump', 'stop') 那么现在我们第一个“模仿”自然语言的程序版本出现了,加上下面针对 JavaScript 的文法: Object :: Identifier Verb :: StringLiteral
二. 实现消息传递 有了文法,一切都好办。看得出来,我们下面的工作是定义能创建一个新 Object 的函数,函数中有一些动作,产生的新 Object 是一个能处理这些消息的函数。创建 Forrest Gump 的函数还可以创建 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 } }) } 为了简单起见还可以把返回的那个函数写成这样: (function (verb) { eval(verb)(); } }) Ok。现在我们来试一试这个智商低于 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! 事情就是这样。我们成功地创造了对象,还让他做动作、说话。 不过,这个实现并不是我们上文中最后一个文法所指出的。它不支持连续发送指令。改一改。要加入顺序执行指令的办法: 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!
三. 利用消息传递处理状态 什么是状态?我们在进行面向对象编程时,把状态表示为对象的一组数据,我们称之为“属性(property)”。在我们的消息传递编程风格中,可以直接把这些数据堆到产生对象的那个函数中去。下面给 Forrest 加入一个状态,Forrest 口袋里的钱。先得声明原先有多少钱: forrest = People(1000) 然后,我们希望可以执行这样的代码,让 forrest 支出 200 美元: forrest('pay', 200) 但很明显,我们无法分清 200 是 Verb 还是 'pay' 所要求的数据。我们只得简化文法,只允许一次发送一个消息,以保全我们的脑细胞: forrest('pay')(200) 也就是说,我们需要让 forrest('pay') 这一表达式返回一个能改变状态的函数,而不仅仅是调用函数来显示一句话。也就是说,如果我们想让 Forrest 急得跳起来,我们先得跳起来: forrest('jump')() 新时代的 Forrest 实现如下(省略了一点多余的代码): 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 当然,我们的 Forrest 还可以赚钱。下面这个版本比较彻底地说明了消息传递编程风格的一切。可以直接修改钱之后,我们可以不需要在创建 Object 的时候就说明原有多少钱;当然,使用注释中的版本更自然: 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
上篇完。小结一下:消息传递的编程风格指的是,把函数 A 的执行上下文当作对象的数据环境,在此定义对象的动词(函数),然后从此上下文中返回一个可以接受、处理消息的函数(常为匿名)。用函数 A 产生消息处理器作为对象,向此对象传递参数作为消息,以此执行函数 A 环境中定义的动作,这些动作还可能改变所在上下文中用一组数据定义的对象状态。
这是最终确定的 JavaScript 基于消息传递编程风格的文章“OOP 诡异教程(上)”的下篇。原来的想法是以风格开头,谈到 JavaScript 的内部机制,但作者 lichray 迟迟没有动键盘,认为不如利用已有的风格做一套机制出来,这样可能更有意义。于是,就有了这个更加“诡异”的下篇。
这篇文章的宗旨是利用我们仅有的“宾谓”语法构造出完整的一套面向对象机制,所以更多代码在更多的时候是不应在实际工作中使用的(也算一种元语言抽象),所以类似效率、代码风格之类的问题反对回帖质疑。
四. 扩展的实现 上文最后给出了一个“看上去很美”的基于消息传递的编程风格,比如构造一个 People 类的代码类似:
function People () { var money = 0 function setMoney (dollars) { money = dollars } function pay (dollars) { money -= dollars } return (function (verb) { return eval(verb) }) }
有了这样的语法我们就可以描述不少句子了。但是存在一个问题:现实中的 Objects 之间是存在关系的——比如,forrest 是个 IQ 为 75 的傻子,傻子是 People 的一种。而我们仅仅是生搬硬套了一种语法而割裂了这种 "is-a" 关系。现在我们的工作,目的之一就是让这样一个“真切”的世界从我们已有的编程风格的地基上拔地而起。 到底应该怎样做才能使 Fool 产生的对象都能响应 People 的消息呢?我们要给 Fool 产生的对象(也就是返回的那个匿名函数啦)都添加这样一种能力:如果在 Fool 中响应不了消息,那就反馈给 People 响应。
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
五. 语法扩展和代码生成 这下代码量增加了很多,强迫潜在的使用者们在创建每个类时都这样写那实在是令人抓狂。本来这篇文章应该不提此类问题的解决,但考虑到有益于读者理解“机制”这个抽象概念,这里给出一个可行的方案——把普通的类代码用 Function() 函数重编译为可用的 JavaScript 函数。也就是说,我们能给出类扩展的代码并指定被扩展的类来获取类似上文的代码:
Fool = extend('People()', function (iq){ var IQ = iq || 0 function init (iq) { IQ = iq } })
为了方便字符串操作,我们希望编译后的代码的参数部分(如 People())都集中出现在一个位置且尽可能便于定位。在函数头添加一句
var origin = People()
当然是可行的,这样还能使 Fool 内部显式引用到其超类。但这样还不够漂亮。我们修改编译后的样例代码为:
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()) }
这个利用参数传递变量的小技巧不值得学习,实际效率不高。但在这篇文章中,这样绑定特殊变量的技术是标准方案。 那么,extend() 函数的实现为:
function extend (originc, code) { function argsArea (code) { // 题外话,正则表达式也有不值得使用的时候 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 的实例代码就可以正常运行了,测试代码不再重复。
六. 机制完备化 这样,我们的基于消息传递编程风格的一套面向对象机制就确定下来了。机制是宪法,是语言的根本***,有了它,我们就可以通过修改代码生成器,很快地给这套机制进行完备化。 想法有很多,例子只举两个。 第一个例子:类的定义中应该能直接引用到将产生的对象 self。答案只有一句话:把返回的那个作为对象的匿名函数命名为 self。 第二个例子:既然是单继承模式,应当存在一个顶层类 AbsObj,使没有指定继承的类自动继承它。答案也只有一句话:在 extend 函数体第一行添加代码:
if (arguments.length == 1) { code = originc originc = 'AbsObj()' }
然后手工构造设计 AbsObj 类,为空也无所谓。不过当然了,一般都会给顶层类添加一些全局性质的消息绑定。由于是“底层操作”,基本上都需要修改 extend 函数。做了一个简单的:
function AbsObj () { //检测是否能响应此 verb,要再用一次异常处理 function canHandle(verb){ try { // 别担心这里的 self 会传递不过去 self(verb) } catch (e) { return false } return true } function toString() {} // 这个搞起来其实很麻烦~` 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
文章写完了,小结一下。消息传递的编程不仅仅是一种代码风格,还可以成长为一种完备的机制。这种完备性远不只是这两篇加起来不到300行的文章所能覆盖的(例如非常彻底的“万物皆对象”,因为只要是能响应消息的函数,连接一下 AbsObj 就是合法对象了;类,函数都可以),大家可以试着玩一玩,顺便体会一下这个计算模型的透明和强大。 另外,熟悉函数式编程的朋友可以帮忙思考一下:这样一个基于闭包变换的计算模型实质上是函数式的,再配合动态的函数式的对象级继承(用一个匿名类代换一下)就能在纯 FP 真正下实现 OOP 了。可惜的是每一次更新操作都要重新生成对象,性能代价大了点,不知道大家有什么好想法。
|