本文为原创, 转载请注明出处.
关于JavaScript中面向对象的编程, 凡是JavaScript的工程师应该有知道原理, 这里不做原理讨论, 只是谈谈自己最近的想法.
我们都知道JavaScript中的this表示的是调用的上下文, 而且可以通过function.call 和 function.apply 来改变调用的上下文. 这两个方法也是构建继承关系的两个非常重要的方法.
同时, 所有OOP的JavaScript框架几乎都会提供一个方法供子类调用超类. 但是所有的这些方法有一些局限性.下面列举一些做为说明, 同时也解释一下我所思考的问题...
假设在有这么几个类(抱歉我比较懒, 就不画类图了)
Permission, UIPermission分别表示权限类, UI权限类.
应用于这样一个场景, 界面上有一个Button,当用户点击Button时执行某一个功能. Permission类用于检测当前用户是否具有操作权限. 只有用户同时具有对这个功能的访问权限和对Button的访问权限时才允许用户点击这个Button, 否则Button对用户不可见.
我们的对象结构如下(JAVA代码), 方法就不声明了, 假设所有的属性都有对应的get,set方法
public class Permission { private String name; private long id; private boolean enabled; public boolean isPermissionAllowed(){ return this.enabled; } }
public class UIPermission extends Permission { private UIControl uiControl; public boolean isPermissionAllowed(long permissionId){ if (super.isPermissonAllowed()) { return this.uiControl.permissoins.contains(permissionId); } return false; } }
这样就实现了一个很简单的逻辑, 当Permission生效, 并且UI控件已经授权, 则权限验证通过, 否则就失败.
现在步入正题, 同样的代码如何用JavaScript来写?
传统的方式:
/**
* UI Permission
** /
function Permission(id, name, enabled){
this.id = id;
this.name = name;
this.enabled = enabled;
}
Permission.prototype = {
id : 0,
enabled : false,
name : null,
isPermissionAllowed : function(){
return this.enabled;
}
}
Permission.prototype.constructor = Permission;
/**
* UI Permission
** /
function UIPermission(){
// 这种代码非常丑陋
UIPermission.superclass.constructor.apply(this, arguments);
// 有没有可能写成这样 ?
// this.superclass.constructor.apply(this, arguments);
}
UIPermission.prototype = new Permission();
// 这两行非常关键, 指明了UIPermission的超类, 并且为prototype也指明了超类.
UIPermission.superclass = Permission;
UIPermission.prototype.superclass = Permission;
UIPermission.prototype.uiControl = null;
UIPermission.prototype.setUIControl = function(uiControl){
this.uiControl = uiControl;
};
UIPermission.prototype.isPermissionAllowed = function(permissionId){
// 同样的, 这段代码有没有可能这样写?
// if (true === this.superclass.isPermissionAllowed.apply(this, arguments)) {
if (true === UIPermission.superclass.isPermissionAllowed.apply(this, arguments)) {
return this.permission.indexOf(permissionId) >= 0;
}
return false;
}
UIPermission.prototype.constructor = UIPermission;
就像我在上面代码中提出的, 有没有可能象在Java中使用super关键字一样, 在JavaScript中使用this.superclass来进行对超类的调用呢?
有些性急的朋友可能已经开始实验了, 然后有可能告诉我: "可以, 我已经试过了".
那么请考虑一下我们目前所做的一切, 我们的继承关系只有一层, 如果我们还有一个类(暂且叫它 XXXPermissoin), 继承自UIPermission. 同样的我们还是要先调用超类的方法, 在执行子类独有的逻辑:
function XXXPermission(){
XXXPermission.superclass.constructor.apply(this, arguments);
// 上面的代码是否还可以写成下面这样?
// this.superclass.constructor.apply(this, arguments);
}
XXXPermission.prototype = new UIPermission();
XXXPermission.superclass = UIPermission;
XXXPermission.prototype.superclass = UIPermission;
XXXPermission.prototype.isPermissionAllowed = function(){
// 这段代码是否还可以写成这样?
// if (true === this.superclass.isPermissionAllowed.apply(this, arguments)) {
if (true === XXXPermission.superclass.isPermissionAllowed.apply(this, arguments)) {
// do something here
return true;
}
return false;
}
这次我相信大家都会说, 不行. 因为很显然会造成死循环.
难道我们真的只能使用如此丑陋的代码来构建我们的对象吗?
下面我们来详细分析一下
造成我们必须使用UIPermission.superclass.constructor.apply(this)的原因是子类在调用超类构造器的时候会将自身(this) 传入超类的构造器, 超类实际执行的上下文实际上是子类的实例. 所以如果超类中使用this.superclass显然是不对的, 因为此时的this已经被apply这个方法改变成子类的实例了, this.superclass实际上就是子类的超类, 也就是超类自己 (此处有点绕, 请仔细理解).
那么我们有没有办法绕过这个限制, 创建一个函数方便的实现继承, 并使用更加优雅的代码?
答案是有可能, 只是要付出一些代价.
下面介绍一下我的实现思路:
1. 声明 extend方法来完成对于继承逻辑的封装, 这也是大多数框架已经做到的. 并不是什么新鲜事.
2. 使用代理构建对象方法, 动态的改变this.superclass所指向的对象 (这一步才是关键).
(没有给出全部代码, 请参考注释自己实现对应的逻辑, 为了方便大家读懂代码, 我把注释从英文改成了中文, 可能有遗漏, 请谅解)
// 第一步, 声明方法代理函数
/**
* create a proxy for a function
*
* @param invocationHandler
* 指定的代理方法, 此代理方法应该包含对原始方法的调用,并返回调用结果
* @param scope
* 为代理方法绑定上下文, 代理方法可以选择使用/不使用此上下文
* @returns {Function}
* 返回包含代理调用的方法, 此方法可以被销毁, 销毁时返回原始方法
*/
Function.prototype.proxy = function(invocationHandler, scope) {
var fn = this;
var proxyFn = function() {
var argsArr = Array.prototype.slice.call(arguments);
return invocationHandler.apply(this, [ scope || this, fn, argsArr ]);
};
// destroy a proxy, return the original function
proxyFn.destroy = function() {
scope = null;
proxyFn.destroy = null;
proxyFn = null;
return fn;
};
return proxyFn;
};
// 第二步, 声明extend函数
/**
* Create an extended class
*
* @param spcls
* 超类
* @param overrides
* 子类实现
* @returns {Function}
* 子类
*/
AS.extend = function(spcls, overrides) {
var sbcls, sbcp, spcp;
var obc = Object.prototype.constructor;
if (overrides && overrides.constructor) {
// 判断超类是否是Object, 如果不是,使用超类构造器
if (obc !== overrides.constructor) {
sbcls = overrides.constructor;
}
}
// 判断子类构造器是否已经赋值
if (!(AS.isDefined(sbcls) && AS.isNotNull(sbcls))) {
// 如果未发现子类构造器, 创建一个构造器
sbcls = function(){};
}
// 为子类附加所有静态方法
AS.apply(sbcls, spcls);
// 声明对于超类的prototype的引用
spcp = spcls.prototype;
// 为子类附加所有实例方法
sbcp = AS.apply({}, spcp);
// 覆盖超类的实例方法
AS.apply(sbcp, overrides);
// 调整子类的构造器
sbcp.constructor = sbcls;
// 声明对超类的引用
sbcp.spcls = sbcp.superclass = spcp;
// 调整对超类构造器的引用
if (spcp.constructor === obc) {
sbcp.spcls.constructor = sbcp.superclass.constructor = spcls;
}
// for IE, 因为IE中有些方法无法被 for .. in .. 循环到
var ieSpecials = ["constructor", "toString"];
/* ==== 关键代码, 循环所有子类方法, 为子类方法创建代理 Start ==== */
for ( var i = 0; i < ieSpecials.length; i++) {
var name = ieSpecials[i];
if (true !== sbcp[name]._class_method_proxy) {
var m = sbcp[name].proxy(AS.classMethodInvocationHandler, sbcp);
m._class_method_proxy = true;
sbcp[name] = m;
}
}
// 为子类方法创建代理方法, 覆盖原有方法
for ( var k in sbcp) {
var m = sbcp[k];
// 如果m是方法, 并且从来没有被代理过
if (AS.isFunction(m) && true !== m._class_method_proxy) {
// 为m创建代理方法, 代理方法的第一个参数绑定到子类的prototype, 详见Function.prototype.proxy
m = m.proxy(AS.classMethodInvocationHandler, sbcp);
// 为已经代理过的方法做标记, 这样同样的方法不会被多次代理
m._class_method_proxy = true;
// 用代理方法替换原始方法
sbcp[k] = m;
}
} /* ==== End 关键代码, 循环所有子类方法, 为子类方法创建代理 ==== */
// apply all static properties to sub-class.constructor
AS.apply(sbcp.constructor, sbcls);
sbcls = sbcp.constructor;
sbcls.superclass = sbcp.superclass;
sbcls.prototype = sbcp;
return sbcls;
};
// 第三步, 创建类方法的代理
/**
* the invocation handler will be used in AS.extend
* @param cls : 预定义的Class, 方法调用应该基于这个Class
* @param method : 调用的原始方法
* @param argsArray : 需要传入原始方法的参数数组
* @returns
*/
AS.classMethodInvocationHandler = function(cls, method, argsArray){
var superclass = this.spcls;
// 如果方法调用的上下文不是预定义的对象, 并且定义了超类
if (this.constructor !== cls.constructor && AS.isDefined(this.spcls)) {
// 判断超类的构造器是否与预定义Class的构造器一致
if (this.spcls.constructor === cls.constructor) {
// 如果一致, 改变当前上下文的超类, 使其指向super.super
this.spcls = this.superclass = cls.superclass;
} else {
// TODO: may have error if goes into this part
throw new Error("superclass is not a defined class");
}
}
// 在当前上下文中调用超类方法, this.superclass可能已经改变
var result = method.apply(this, argsArray);
// 方法执行结束, 将this.superclass指向原始值
this.spcls = this.superclass = superclass;
// 返回执行结果
return result;
};
// 其它辅助方法
AS.apply = function(target, source, defaults) {
if (defaults) {
AS.apply(target, defaults);
}
if (source) {
for ( var i in source) {
target[i] = source[i];
}
}
return target;
};
经过以上处理, 我们就可以使用在任意子类的声明中使用this.superclass.constructor.apply(this, arguments) 来调用超类了. 下面附上测试用例:
/* ===== test AS.extend ===== */
function User(name) {
this.name = name || '';
info("name pass in User.constructor is " + name);
}
User.STATUS = {};
User.prototype = {
names : [],
getName : function() {
return this.name;
},
setName : function(name) {
this.name = name;
this.names.push(name);
}
};
function testASExtend() {
var Client = AS.extend(User, {
constructor : function() {
this.spcls.constructor.apply(this, arguments);
}
});
var Lead = AS.extend(Client, {
constructor : function(){
this.spcls.constructor.apply(this, arguments);
}
});
var userName = "user";
var clientName = "client";
var user = new User();
var client = new Client();
var lead = new Lead();
assertEquals("User.STATUS == Client.STATUS", User.STATUS, Client.STATUS);
assertEquals("User == user.constructor", User, user.constructor);
assertEquals("User == user.prototype.constructor", User,
User.prototype.constructor);
assertEquals("Client == client.constructor", Client, client.constructor);
assertEquals("Client == Client.prototype.constructor", Client,
Client.prototype.constructor);
assertEquals("User.prototype == Client.prototype.superclass",
User.prototype, Client.prototype.superclass);
assertEquals("User.prototype == Client.superclass", User.prototype,
Client.superclass);
assertEquals("User.prototype == Client.prototype.spcls", User.prototype,
Client.prototype.spcls);
assertEquals("User == Client.prototype.spcls.constructor", User,
Client.prototype.spcls.constructor);
client.setName(clientName);
user.setName(userName);
assertEquals("userName == user.getName()", userName, user.getName());
var v = clientName === client.getName();
assertTrue("clientName == client.getName()", v);
}
// 测试多级继承
function testASDeepExtend() {
// Lead继承自User
var Lead = AS.extend(User, {
name : '',
constructor : function() {
// 这里使用this.superclass访问超类
this.spcls.constructor.apply(this, arguments);
}
});
// Client继承自Lead
var Client = AS.extend(Lead, {
constructor : function() {
// 这里使用this.superclass访问超类
this.spcls.constructor.apply(this, arguments);
}
});
var user = new User("user");
var lead = new Lead("lead");
var client = new Client("client");
assertTrue("Client.prototype.name should extend from lead", AS
.isDefined(Client.prototype.name));
assertEquals("user.getName should return user", "user", user.getName());
assertEquals("lead.getName should return lead", "lead", lead.getName());
assertEquals("client.getName should return client", "client", client.getName());
}
function testASExtendAndOverride() {
var userName = "user";
var clientName = "client";
var getNameScope;
var setNameScope;
var Client = AS.extend(User, {
constructor : function(name) {
info("name pass in Client.constructor is " + name);
this.spcls.constructor.apply(this, arguments);
info("inside Client.constructor");
},
setName : function() {
info("inside Client.setName");
setNameScope = this;
this.spcls.setName.apply(this, arguments);
},
getName : function() {
info("inside Client.getName");
getNameScope = this;
return this.spcls.getName.apply(this, arguments);
}
});
var user = new User(userName);
var client = new Client(clientName);
assertEquals('Client constructor equals with instance', Client,
client.constructor);
assertEquals('Client constructor equals with prototype', Client,
Client.prototype.constructor);
assertEquals(userName, user.getName());
assertEquals(clientName, client.getName());
client.setName();
assertEquals(client, getNameScope);
assertEquals(client, setNameScope);
}
总结: 我的思路是使用了代理来代理子类方法的调用, 在方法调用前判断当前实例的构造器是否与代理方法绑定的构造器一致. 如果一致说明方法是在声明类中执行的, 否则说明方法是在超类中执行的, 此时应该动态调整this.superclass使其指向this.superclass.superclass, 这样可以保证在使用apply或call方法时, 仍然可以找到正确的superclass.
优点:
1. 使用了更友好的语义来定义类
2. 使用了更友好的语义来进行超类的调用
缺点: 由于使用了代理, 所以会造成额外的内存开销, 因此代理方法本身提供了destroy方法来销毁内存.
其它:
我现在使用构造器来判断执行上下文是否是预定义的类实例. 此方法有一个缺陷, 就是无法对frame中嵌入的脚本执行相同的判断. 如果有人不了解这一点, 请查阅相关资料.
所以如果我们采用另一种声明方式为每一个类附加独有的标志比如User.prototype.classType = 'User', 然后判断调用的上下文的classType是否和预定义的classType一致, 效果可能更好.
好了, 到此结束, 欢迎讨论