- 来自:http://corodidea.net/blog/index.php/archives/129
- 作者
- infinte
- 关键词
- JavaScript, OOP, 框架, 运行时覆写, 保护成员
- 摘要
-
本文主要实现了一个轻量的JavaScript OOP框架,并借此讨论了基于运行时覆写实现基类方法调用的技术以及实现保护成员的方法。
A lightweight JavaScript OOP framework was implemented in this text. Base-method invocation implementation technique, using runtime overwriting, and implementation of protected members was discussed here.
我终于这个优雅且几乎完美的JavaScript OOP 实现——轻便,简洁,并且有效。(暂且命名为emop)
Finally I've found the elegant implementation of almost-perfect OOP framework implementation in Javascript. It's light, clear, and, effective.(temporarily named emop)
- var Class;
- void function() {
- var mgBaseGen = function(bcl, original) {
- return function(nname) {
- var c = bcl, rv;
- while (c && !c.prototype.hasOwnProperty(nname)) c = c.baseConstructor;
- if (!c) throw new Error('Cannot find base class with specific methods');
- this.base = mgBaseGen(c.baseConstructor, this.base);
- try {
- rv = c.prototype[nname].apply(this, Array.prototype.slice.call(arguments, 1));
- } finally {
- this.base = original;
- };
- return rv;
- };
- };
-
- var CHECK = {};
-
- var NULLFUNC = function() {};
-
- var HOPCHECKER = {};
-
- var protectedName = '..protected..';
-
- var registerProtecteds = function(o) {
- var protecteds = [];
- var p = function(checker, name, value) {
- if (checker !== CHECK) throw new Error('access denied');
- if (arguments.length > 2) return protecteds[name] = value;
- else if (HOPCHECKER.hasOwnProperty.call(protecteds, name)) return protecteds[name];
- };
- p.length = 0;
- p.toString = function() {
- throw new Error('access denied')
- };
-
- o[protectedName] = p;
- };
-
-
-
- var bound = null;
- var priv = function(n, v) {
- if (!HOPCHECKER.hasOwnProperty.call(bound, protectedName)) {
- if (protectedName in bound) {
- registerProtecteds(bound);
- } else {
- return new Error('access denied');
- }
- }
- if (arguments.length > 1) return bound[protectedName](CHECK, n, v);
- else return bound[protectedName](CHECK, n);
- };
-
- var wrapProtected = function(method) {
- return function() {
- var hold = bound;
- bound = this;
- try {
- var ret = method.apply(this, arguments);
- } finally {
- bound = hold;
- return ret;
- }
- }
- }
-
-
- var NClassBase = null;
- var NAttachments = [];
-
- Class = function(Definition, proto) {
-
-
- var BClassBase = NClassBase;
- var BAttachments = NAttachments;
-
-
- NAttachments = [];
-
- try {
-
- var ctor, def = new Definition(priv);
- if (def.initalize instanceof Function) {
- ctor = wrapProtected(def.initalize);
- } else {
- ctor = NULLFUNC;
- };
- delete def.initalize;
-
-
- var Clz = function() {
- registerProtecteds(this);
- return ctor.apply(this, arguments);
- };
-
-
- if (proto instanceof Function || NClassBase instanceof Function) {
- if (proto instanceof Function) {
- Clz.prototype = new proto;
- Clz.baseConstructor = proto;
- } else if (NClassBase instanceof Function) {
- Clz.prototype = new NClassBase;
- Clz.baseConstructor = NClassBase;
- };
- Clz.prototype.base = function(name) {
- var c = Clz.baseConstructor, rv, cler = arguments.callee.caller.caller;
- var original = this.base;
- this.base = mgBaseGen(c, original);
- try {
- rv = this.base.apply(this, arguments);
- } finally {
- this.base = original;
- };
- return rv;
- };
- } else {
- if (proto) Clz.prototype = proto;
- else Clz.prototype = {};
- Clz.baseConstructor = Object;
- };
-
- proto = Clz.prototype;
-
-
- for (var i = 0; i < NAttachments.length; i++) {
- var item = NAttachments[i][0];
- var args = [proto].concat(NAttachments[i][1]);
- item.apply(proto, args);
- }
-
-
- for (var each in def) if (def.hasOwnProperty(each)) {
- if (def[each] instanceof Function) {
- var f = wrapProtected(def[each]);
- f.toString = function() {
- return def[each].toString()
- };
- f.length = def[each].length;
- proto[each] = f;
- } else {
- proto[each] = def[each];
- }
- };
-
- } finally {
- NClassBase = BClassBase;
- NAttachments = BAttachments;
- return Clz;
- };
- };
-
- Class.base = function(f) {
- NClassBase = f;
- };
- Class.attach = function(attachment, _args_) {
- NAttachments.push([attachment, Array.prototype.slice.call(arguments, 1)]);
- };
-
- Function.prototype.derive = function(definition) {
- return Class(definition, this);
- };
- Function.prototype.asClass = function(base) {
- return Class(this, base);
- }
-
- }();
-
-
- var Greeter = function() {
- this.greet = function() {
- alert('hello!');
- }
- }
-
- var A = function(priv) {
- Class.attach(Greeter);
- this.f = function(t) {
- priv('y', t);
- alert(priv('y'));
- };
- }.asClass();
-
- var B = A.derive( function(priv) {
- var Point = function(p) {
- this.initalize = function(x, y) {
- p('x', x);
- p('y', y);
- };
- this.abs = function() {
- var x = p('x');
- var y = p('y');
- return Math.sqrt(x * x + y * y);
- };
- }.asClass();
- this.g = function(p, q) {
- alert((new Point(p, q)).abs());
- };
- this.f = function(t) {
- this.base('f', t);
- alert('-- ' + t);
- }
- this.inter = function(obj, t) {
- obj.f(t)
- };
- });
-
- var foo = new B();
- foo.greet();
- foo.f(2);
- var foo2 = new B();
- foo2.g(5, 5);
- foo2.inter(foo, 3);
这是什么?
What is it?
长期以来JavaScript程序员感到JavaScript的诸多掣肘之一就是它没有“完整的”OOP支持。尽管JavaScript利用原型系 统提供了OOP功能,但它没有提供这些特性:保护成员和基类方法调用。这段代码就是为了解决此问题。
For a long time, one of its limits which JavaScript programmers found is that it cannot provide "complete" OOP support. Although OOP was provided by the prototype system, the following charactics are absent: protected members and base method invocation. The target of this code is to solve this problem.
基本思想
Basic concepts
- 使用闭包独立环境
Isolate environments by using closures
- 运行时覆写
Runtime overwriting
- 使用try-finally的异常处理
Exception handling via try-catch
定义类
Defining class
定义类的是一个函数,和传统的JavaScript构造器类似:
What defines a class is a function, written in thetraditional way:
- function(priv) {
- this.g = function(t) {
- priv('x', t);
- alert(priv('x'));
- };
- this.f = function(t) {
- this.base('f', t);
- alert('-- ' + t);
- }
- }
你可以看见类似传统“构造器”的东西——用this.methodName定义方法。不过,这里有一个很奇怪的priv参 数——它就是保护乘员的读写器。类定义将会传入Class函数,经过一系列复杂的转化后,返回我们需要的构造器。获得这个类只 要通过下面一句:
You can see something like traditional JavaScript constructors--defineing methods via "this.methodName". But, a strange method named "priv" appears here.It's the accessor of protected members. Such class definition will be sent into Class, and after a series of complicated transformation, the constructor we need yields.Getting this class can even use only one statement:
- A = Class(definition, base)
第一个参数是定义,第二个是其基类。
The first argument is the definition, the second is its base class.
emop还有两个语法糖:一个是Function.prototype.asClass,另一个是Function.prototype.derive。 它们分别定义为:
There are also 2 syntax sugars in emop: one is Function.prototype.asClass, the other is Function.prototype.derive. Their definitions are:
- Function.prototype.derive = function(definition) {
- return Class(definition, this);
- };
- Function.prototype.asClass = function(base) {
- return Class(this, base);
- }
细节
In depth
你已经看到了,一个类定义很像一个构造器,但事实上不是。类定义是个函数,而且真的会运行,但只会运行一次。事实上构造器是由其定义的initalize方 法描述:
You have seen that a class definition looks like a constructor, but it in't. Class definition is a function, and it will be invoked, but only once. Actually, the exact constructor is described by initalize method which the definition defines:
- var Point = Class(function(p) {
- this.initalize = function(x, y) {
- p('x', x);
- p('y', y);
- };
- this.abs = function() {
- var x = p('x');
- var y = p('y');
- return Math.sqrt(x * x + y * y);
- };
- });
上面的Point是一个极好的例子。当我们要构造一个点的时候,虽然代码像下面的这样:
The Point above is a good example. When we need to construction a point, the following code appears:
- var p1 = new Point(0, 0);
特性
Charactics
你可能已经发现第一段代码中有几个很有趣的方法:
Maybe you've found some interesting methods in the first code paragraph:
Class.attach
Class.base
“priv”
(instance).base
上面几个“特殊”方法的用途分别是:
Usages of the "special" methods are:
- 附加相关的“附件”(混入类)
Attach specific "Attachments"(mixins)
- 指定基类
Define base class
- 存取“保护成员”
Access "protected members"
- 调用“基类”方法
Invoke base methods
priv
因为类定义是一个函数,它的第一个参数就是存取保护成员的读写器。在emop中一个对象拥有一般成员(和JavaScript中的一样)和保护成员 (只能用这个读写器访问)。因为方法定义总是包含在类定义中,所以priv可以被访问到,对象方法可以使用priv读 写保护成员:
Because class definition is a function, it's first paramater is the accessor of protected memners. Objects in emop contains normal members (like that in native JavaScript object) and protected members(only accessable by this accessor). Because the method definition is always contained by class definition, priv can be accessed and methods can use it to access protected members:
- this.m = function() {
- priv('x');
- priv('x', 1);
- }
在这个例子中,m方法可以自由读写'x'保护成员。但priv在 类定义外就不能访问了,所以外界不能经过priv读写保护成员。
In this example, method m can access protected member 'x' freely. But priv is not accessable outside the class definiton, so outside program cannot access protected members via priv.
priv有两种调用方法:
There are two ways to invoke priv:
- priv('x');
- priv('x', 1);
传入一个参数意味着读取,传入两个参数就是写入。
Passing one argument means reading, two means writing.
因为是保护成员,所以基类定义的方法和派生类定义的方法都可以使用priv访问。
Because it is protected members, base methods and derived methods can both access it via priv.
- var A = Class( function(p) {
- this.f = function() {
- p('x', 1)
- }
- });
- var B = Class( function(p) {
- this.g = function() {
- return p('x')
- }
- },A);
-
- var obj = new B();
- obj.f();
- obj.g();
Class.base and Class.attach
emop规定一个类只有一个基类,Class.base就描述基类。而Class.attch则 描述了混入类。混入类定义类似类定义,但它是直接apply到某类的prototype上。显然, 混入类不能拿到保护成员读写器priv。上面的那段代码可以改写成:
emop declared that a class have only one base class, and it is defined by Class.base. Class.attach describes mixins. Mixin definition looks like class definition, but it is directly apply-ed to the prototype of some class. Obviously, mixins cannot get protected member acceessor "priv". Code above can be written like:
- var A = Class( function(p) {
- this.f = function() {
- p('x', 1)
- }
- });
- var B = Class( function(p) {
- Class.base(A);
- this.g = function() {
- return p('x')
- }
- });
-
- var obj = new B();
- obj.f();
- obj.g();
因此,定义派生类有两种方法:
So, derived class can be defined in two ways:
- Derived = Class( function() {......
- },Base)
- Derived = Class( function() {
- Class.base(Base);......
- })
下面的情况下,Base1是Derived的基类。另一个被抛弃:
Under the following situation, Base1 is the base class of Derived, Not Base2
- Derived = Class( function() {
- Class.base(Base2);......
- },Base1)
混入类可以参数化——当调用Class.attach(mixin,a,b,c)时,传入mixin的 第一个参数是被定义类的prototype,接着是a,b,c。传入的this指 针也是那个prototype:
Mixins can be parameterized -- When invoking Class.attach(mixin,a,b,c), First argument passed into mixin is the prototype of defined class, following arguments are a,b and c. this pointer passed is also that prototype.
- Mix = function(proto, a) {
- this.f = function() {
- return a
- }
- };
-
- var Thing = Class(
- function() {
- Class.attach(Mix, 1);
- });
-
- var t = new Thing;
- t.f()
(instance).base
每个用Class创建的类构造的对象都有一个base用于调用基类方法。类似这样:
Each object constructed by a Class-defined class contains the base as a base-method invoker. Like this:
- var A = Class( function(p) {
- this.f = function(t) {
- return t
- }
- });
- var B = Class( function(p) {
- Class.base(A);
- this.f = function(t) {
- return 'x' + this.base('f', t)
- };
- });
-
- var obj = new B();
- obj.f(1);
传入给base的第一个参数是要调用的基方法名,接着是参数。base会搜索其基类,如果它定义了 指定的方法,就执行之,否则继续向上,直到确认在所有基类中都找不到为止。
First argument passed into base is the method name we want to invoke, then "actual" arguments. base will search its base class, invoke the specified method if the base class defined it, or continue going up until it confirmed the method is absent in all base class definitions.
- var A = Class( function(p) {
- this.f = function(t) {
- return t
- }
- });
- var B = Class( function(p) {
- Class.base(A);
- });
-
- var C = Class( function(p) {
- Class.base(B);
- this.f = function(t) {
- return 'x' + this.base('f', t)
- }
- this.g = function() {
- return this.base('g')
- }
- });
-
- var obj = new C();
- obj.f(1);
- obj.g();
实现技术
Implementation Techniques
运行时覆写
Runtime Overwriting
回溯法的例子
Examples in backtracking algorithms
想象一下我们在书写一个迷宫程序。迷宫由若干cell组成,我们的目的是看有没有去出口的路径。这种问题很适合用回溯 法,算法的框架大体如下:
Imagine that we're writing a maze program. A maze consists of some cell, and we're going to find out whether the way to the exit exists. This problem is suit for backtracking. The algorithm looks like this:
- var found = false, steps = 0;
-
- function searchCell(position) {
- position.visited = true;
- if (position == maze.exit) {
- found = true;
- return
- };
- steps += 1;
- var neighbors = position.getNeighbors();
- for (var each in neighbors) if (!neighbors[each].visited) {
- searchCell(neighbors[each]);
- if (found) return;
- }
- steps -= 1;
- }
-
- searchCell(maze.start)
steps很好地说明了运行时覆写的原理——当搜索一个未访问的cell时,把它加1,表示 “走一步”;而回溯的时候,则把它减1,表示“退回”。这样,如果你搜索地图无果,那么steps还是0。
steps explained the mechanism of runtime overwriting well -- when searching one unvisited cell, increase it, means "one step"; when backtracking, decrease it, means "go back". Therefore, if the way does not exist, steps equals to 0.
运行时覆写的基本思路就是——当进入方法,修改某个东西;当退出方法,则把它改回来。
Basic principles of Runtime overwritting is Change something when enter the method, and change it back when exit.
priv
你肯定很好奇调用priv的时候是怎么知道要取那个对象的保护成员的。要知道我们压根没有给priv传this指 针。实际上,priv取的是一个叫bound的对象的保护成员——bound则 是在调用对象方法的时候覆写成传入那个方法的this指针的。
You will be curious about how priv knows which object's protected members will be accessed. We never pass this pointer to priv. Actually, priv accesses the protected members of an object named "bound". "bound" is overrited to this pointer when invoking a method.
emop中,每个类定义的方法都被包裹,秘密在wrapProtected中。它的代码是:
- var bound;
-
-
- var wrapProtected = function(method) {
- return function() {
- var hold = bound;
- bound = this;
- try {
- var ret = method.apply(this, arguments);
- } finally {
- bound = hold;
- return ret;
- }
- }
- }
-
- Clz.prototype[each] = wrapProtected(def[each]);
因为不能给priv直接传this(否则要他还有何意义),wrapProtected就 做了一个有趣的事情——在对象调用方法之前,把bound设置成传入的this指针,同时做备份; 而当退出时,则把bound再改回来。这么一来二去,method调用时priv可 以正确找到保护成员,而结束后则像什么都没发生一样。
Because we can't pass this to priv explicitly, wrapProtected did something interesting. Before the "real" method's invocation, change bound to passed this pointer, also backup it; After the method invoked, change bound back. With this process, when the "real" method is running, priv works correctly, and, when it ended, it seems nothing happened to priv and bound.
try ... finally保证了即使method发生异常,bound也 能改回来。当然,这会造成一点性能损失。
try ... finally ensured that even method throws an exception, bound will be changed back. Obviously, it will slow down the progeam a little.
Class.base和Class.attach的原理相似,这里就不深究了。各位可以看代 码研究。
Class.base and Class.attach works in the same mechanism. You can read the code above.
base
base中使用的技术最为复杂——因为这次覆写的就是它自己。this.base在它自己调 用时,基类方法调用前覆写,基类方法调用后改回。
Techniques used in base is the most complicated, becaust it will overwrite itself. this.base overwrites itself after it is invoked, and before base method's invocation; and change it back after base method exits.
base搜索基方法依赖在类上维护的baseConstructor属性,它直接指向基类。用于覆写的base由一个叫mgBaseGen的函数生 成,代码是:
base's searching depends on the baseConstructor property of classes, which points to it's class. bases used to overwrite is generated by function mgBaseGen:
- var mgBaseGen = function(bcl, original) {
- return function(nname) {
- var c = bcl, rv;
- while (c && !c.prototype.hasOwnProperty(nname)) c = c.baseConstructor;
- if (!c) throw new Error('Cannot find base class with specific method');
- this.base = mgBaseGen(c.baseConstructor, this.base);
- try {
- rv = c.prototype[nname].apply(this, Array.prototype.slice.call(arguments, 1));
- } finally {
- this.base = original;
- };
- return rv;
- };
- };
mgBaseGen接受两个参数,一个是搜索方法的起点bcl,另一个是备份original。 在3-5行,沿着baseConstructor链搜索某个合适的类c,包含nname指 定的方法;接着,把this.base覆写成以c的基类为起点的新base。 然后,调用基类方法。调用完毕,则把this.base改回original,清除痕迹;最后,返 回值。
mgBaseGen accepts two parameters, ont is the searching start, bcl, the other is the backup, original. In line 3-5, generated base method searches the class c defines specified method method; then overwrite the this.base method by a new base with c.baseConstructor as searching start. Then, invoke the found base method and change this.base back.
这样,this.base就以一种神奇的方式工作——如果基类方法还要调用更“基类”的方法的话,this.base就 会站在更深的地方搜索方法。而当基类方法调用完成时,this.base就像没改过一样。
In this way, this.base works magically -- if the base method needs to invoke a "baser" method, this.base will start searching the method at a "deeper" place. But when the base method is exited, this.base looks unchanged.
this.base加入到原型中使用下面的代码——这几乎不用解释:
Adding this.base into prototype uses following code -- explaining is needless:
- Clz.prototype.base = function(name) {
- var c = Clz.baseConstructor, rv, cler = arguments.callee.caller.caller;
- var original = this.base;
- this.base = mgBaseGen(c, original);
- try {
- rv = this.base.apply(this, arguments);
- } finally {
- this.base = original;
- };
- return rv;
- };
类定义的实现
Implementation of class definition
在类定义中我们用“this.method = function(){...}”定义方法,但是,这种定义法是如何变 成一个构造器的呢?事实上在Class中,我们把定义给new了一次:
In class definitions, we use "this.method = function(){...}" to define methods. But how did the class definition come into a constructor. In fact, we constructed the definition once in Class:
- Class = function(definition, proto) {
-
- var ctor, def = new Definition(priv);
- if (def.initalize instanceof Function) {
- ctor = wrapProtected(def.initalize);
- } else {
- ctor = NULLFUNC;
- };
- delete def.initalize;
-
-
- var Clz = function() {
- registerProtecteds(this);
- return ctor.apply(this, arguments);
- };
-
- }
def对象是“new”定义的产物,接着,如果它包含initalize方 法,则把包裹它作为“初始化器”,否则,就用空函数。生成的构造器Clz实际上包含了两步,一个是注册保护成员(见下一节), 另一个则是执行初始器。
The def object is the result of constructing definition, and then, if def contains initalize method, wrap it by wrapProtected and set the initalizer (ctor) to it; if not, set it to the empty function. Generated constructor Clz contains 2 steps: registering protected menber (see the selection below), and executing ctor.
原型的绑定在下一步:
Prototype is bound in the next:
-
- if (proto instanceof Function || NClassBase instanceof Function) {
- if (proto instanceof Function) {
- Clz.prototype = new proto;
- Clz.baseConstructor = proto;
- } else if (NClassBase instanceof Function) {
- Clz.prototype = new NClassBase;
- Clz.baseConstructor = NClassBase;
- };
- Clz.prototype.base = function(name) {
- var c = Clz.baseConstructor, rv, cler = arguments.callee.caller.caller;
- var original = this.base;
- this.base = mgBaseGen(c, original);
- try {
- rv = this.base.apply(this, arguments);
- } finally {
- this.base = original;
- };
- return rv;
- };
- } else {
- if (proto) Clz.prototype = proto;
- else Clz.prototype = {};
- Clz.baseConstructor = Object;
- };
-
- proto = Clz.prototype;
这里检查了两处,一个是显式传入的proto,另一个是用Class.base注册的基类NClassBase。 如果proto是函数,则构造一个对象做原型,NClassBase同理。如果proto是 一个对象,则把Clz的原型直接赋予它。
We checked explicitly passed argument proto and NClassBase registered by Class.base. If proto is a function, the prototype of Clz will be the object constructed by proto. NClassBase is processed in like manner. If proto is an object, but not a function, then directly set Clz.prototype to it.
保护成员的实现
Implementation of protected members
现在我们要深入priv,了解保护成员是如何实现的。
Now we will see the detail of priv, to know how protected members implemented.
- var priv = function(n, v) {
- if (!bound.hasOwnProperty('__protected')) return new Error('access denied');
- if (arguments.length > 1) return bound.__protected(CHECK, n, v);
- else return bound.__protected(CHECK, n);
- };
priv的实现异常简单——它调用……对象的__protected方法。等等,这个方法会不会把 保护成员泄密?实际上不会,因为有CHECK。CHECK是一个围在闭包里的“令牌”,外界几乎无 法获取。__protected方法是在这里注册的:
The implementation of priv looks very simple. It invokes ...... the __protected method of the object. But will this method betray its protected methods? Actually no, because there is a check. The constant CHECK is an token object contained in the outer closure and it's unaccessable outside. __protected was registered here:
-
- var CHECK = {};
-
- var NULLFUNC = function() {};
-
- var HOPCHECKER = {};
- var registerProtecteds = function(o) {
- var protecteds = [];
- var p = function(checker, name, value) {
- if (checker !== CHECK) throw new Error('access denied');
- if (arguments.length > 2) return protecteds[name] = value;
- else if (HOPCHECKER.hasOwnProperty.call(protecteds, name)) return protecteds[name];
- };
- p.length = 0;
- p.toString = function() {
- throw new Error('access denied')
- };
-
- o.__protected = p;
- };
函数p的第一行干的就是检查CHECK,如果“令牌”不对,对不起,access denied。接下来的内容就很简单了,基本的hash操作。这里使用了一个小技巧,用HOPCHECKER的hasOwnProperty检 查protecteds的属性有无,防止覆写hasOwnProperty造成检测的错误。
What the first line of function p did is checking CHECK. if the token is wrong, then throw an access denied exception. Following contents are easy, basic hashtable operations.There is a small technique: checking property existance of protecteds by HOPCHECKER's hasOwnProperty, in order to avoid mistakes made by overwriting hasOwnProperty.
那么,registerProtecteds是何时“注册”到对象上的呢?答案在这里:
So, when does registerProtecteds "registers" to the object? The answer is here:
- var Clz = function() {
- registerProtecteds(this);
- return ctor.apply(this, arguments);
- };
优势
Advantages
- 仍可以使用原生的
Clz.prototype.method = function(){}定义方法
Native Clz.prototype.method = function(){...} is still avliable
base在不用Class定义的派生类中仍可使用,只需维护baseConstructor
base is still avaliable in derived classes not defined by Class, only keeping baseConstructor needed.
- 支持嵌套类
Nested classes supported
待改进
To do
- 使用
try ... finally造成调试不便
try ... finally will make debugging unconvient
- 性能损失
Preformance lack
- 不支持ECMA v5定义的读写器,稍加修改即可
Accessor defined in ECMA v5 is not supported, but it can be supported after a little modification.