OOP 诡异教程 (上)
2007-06-08 12:40:07 来自: 氷の鋭(神不为者,人为之)
本文分上、下两篇,站在一个难以名状的角度上研究了 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 环境中定义的动作,这些动作还可能改变所在上下文中用一组数据定义的对象状态。
(To be continue)
> 我来回应
这个小组的成员也喜欢去 · · · · · ·

- 上海趣事 (29)

- 扯淡er (470)

- 完美web (759)

- WEB标准 (2837)

- CSharp (24)

- AJAX (1747)
最新话题:
ECMAScript-262 翻译那事怎么样了? (justin)
OOP 诡异教程 (上) (氷の鋭)
OOP 诡异教程(下) (氷の鋭)
Flash格式的Javascript PPT在线阅读 (xinhe)
大家一起来翻译 MDC! (氷の鋭)
第十章:执行上下文 已发布 (氷の鋭)