我看javascript的面向对象

昨天看到一篇很老的文章,讲的是javascript的面向对象实现。

http://www.cnblogs.com/sanshi/archive/2009/07/08/1519036.html

不过作者给出的方案、以及文中列出的另外两个方案都很不能让人满意,于是开始思考怎么去实现会比较好。

文中的方案的共同点,也是一个让我不太舒服的点就是对于子类向父类继承属性的处理方式。基于js天然的prototype,类与类间的继承关系很好实现,直接把子类的构造函数的prototype指向父类的实例即可实现子类的实例可以直接调用父类的方法与属性。但问题在于,prototype的概念本来就不是为了实现面向对象的(我是这么认为的)。子类的所有实例公用一个构造函数,因而共用一个原型对象(prototype)。这样导致的问题是如果在实现子类的构造函数的时候将其prototype指向的父类实例初始化,则所有子类的实例对象共用同一个原型对象(即该父类实例)的属性。

文章中给出的实现方案大体思路如下:首先,在生成实例时,将构造函数与实例的初始化(添加属性)分开,即用constructor构造对象,然后用另一个函数,如init初始化对象。接着使用一个变量,如initializing指示当前是否处于类的创建阶段,若是,则当前生成的对象只构造而不初始化。最后在实现子类实例时,通过对父类初始化方法的调用实现初始化。这样做的结果是,父类的初始化方法时所添加的属性不是添加到父类实例上,而是添加到此时正在实现的子类实例上(因为此时调用父类的初始化函数的不是该父类实例,而是当前的子类实例)。这样所有的子类实例都拥有了父类的属性,而且互不共用。

为什么我觉得这不好?

  1. 采用一个额外的、用于指示状态的变量initializing这本身就很奇怪。这是可以避免的。

  2. 每个子类实例拥有自己的属性,即使是从父类继承的,也无法在实例中体现这一点。我觉得好的继承实现,不仅在于类的层面,而且在于实例的层面都应该体现继承性。

那怎么做?

其实我想说说js天生就是基于对象的,也有着自己的编程模式(原型和函数的模式, the prototypal and functional patterns),没必要去生搬硬套面向对象的那套。但是这样很不厚道,喷了人家一通,然后说其实我没什么想法。所以我还是要尝试一下。

首先,我参考了公司的框架(以下简称Q)中对于类的实现,获益匪浅。

在于处理上面提到的子类向父类继承属性这一点上,Q的做法跟上面说到的思路有的不同之处在于:不采用额外的变量指示当前状态,而是在将子类的构造函数的prototype指向新生成的父类实例之前,删除父类实例中的已有属性(虽然不得不说,我觉得这还不如采用一个指示变量)。接着,在实例化子类对象时,框架并不做向子类实例初始化父类属性的事,而是通过将基类(不是别的类的子类)的原型指向一个添加了自定义方法callsuper的对象,给所有的类callsuper的方法。这个方法调用当前类的父类的方法(包括构造函数)。所以在定义子类时,在子类的初始化方法中添加callsuper(某参数)即在子类实例初始化时上实现父类的初始化过程。

贴代码(Q的做法):

Class:

var Class = function(name,data){
	var ns = (data.ns) && data.ns + '.' + name;
	if(ns){
			try{
					var exist = (new Function("return " + ns))();
					if(exist)return exist;
			}
			catch(e){};
	}
	var superclass = data.extend || _Object;
	var superproto = function(){};
	var plugins = data.plugins || [];
	superproto.prototype = superclass.prototype;
	var constructor = data.construct || function(){};
	var properties = data.properties || {};
	var methods = data.methods || {};
	var statics = data.statics || {};
	var proto = new superproto();
	for(var key in proto){
			if(proto.hasOwnProperty(key)){
					delete proto[key];
			}
	}
	for(var key in properties){
			proto[key] = properties[key];
	}
	for(var key in methods){
			proto[key] = methods[key];
	}
	for(var i = 0; i < plugins.length; i++){
			var plugin = plugins[i];
			for(var key in plugin){
					proto[key] = plugin[key];
			}
	}
	proto.constructor = constructor;
	proto.superclass = superclass;
	proto.superinstance = new superproto();
	proto.__NAME__ = name;
	constructor.prototype = proto;
	for(var key in statics){
			constructor[key] = statics[key];
	}
	if(ns){
			_ns(ns,constructor);
	}
	return constructor;
}

callsuper:

proto.callsuper = function(methodName){
		var _this = this;
		/* 在一次调用过程中,逐级记录父类引用,保证正确调用父类方法。不支持在异步过程中调用callsuper */
		if(!this._realsuper){
				this._realsuper = this.superclass;
		}
		else{
				this._realsuper = this._realsuper.prototype.superclass;
		}
		if(typeof methodName == 'string'){
				var args = Array.prototype.slice.call(arguments,1);
				_this._realsuper.prototype[methodName].apply(_this,args);
		}
		else{
				var args = Array.prototype.slice.call(arguments,0);
				_this._realsuper.apply(_this,args);

		}
		this._realsuper = null;
};
_Object.prototype = proto;

唔,我想说,虽然这个做法不咋地,但是跟先前说的的方法思路差别很大,给我很大启发。最后是我的尝试:

var Class = function(baseClass, opt){
		/*
			* opt: {
			*     init: function(){},
			*     methods: [...]
			* }
			*/

		//无baseClass的情况
		if(!opt){
				opt = baseClass;
				baseClass = function(){};
				if(!opt || typeof opt !== 'object'){
						opt = {};
				}
		}

		var init = opt.init || function(){};
		var proto = {};

		//将包装后的父类方法添加到子类的原型对象上,跟前面实现的不同主要在此
		var baseClassProto = baseClass.prototype, func;
		for(var name in baseClassProto){
				if(baseClassProto.hasOwnProperty(name)){
						func = baseClassProto[name];
						if(typeof func === 'function'){
								proto[name] = (function(f){
										return function(){
												return f.apply(this.__base__, arguments);
										};
								})(func);
						}
				}
		}

		//将参数中的方法添加到子类的原型对象上
		if(opt.methods){
				var methods = opt.methods;
				for(var name in methods){
						if(methods.hasOwnProperty(name)){
								proto[name] = methods[name];
						}
				}
		}

		//可以选择传递一个父类实例实现实例层面的继承关系
		var constructor = function(baseObj, opt){
				if(arguments.length >= 2 && baseObj.constructor !== baseClass){
						throw new Error('Wrong Base Object!');
				}
				if(!baseObj || baseObj.constructor !== baseClass){
						opt = opt || baseObj;
						baseObj = new baseClass(opt);
				}

				this.__base__ = baseObj;

				init.call(this, opt);
		};

		proto.constructor = constructor;
		constructor.prototype = proto;

		return constructor;
};

最后说说这样实现的优点跟缺点:

优点:避免引入指示状态的额外变量,实现实例层面的继承关系,子类中属于父类的属性不是直接添加在子类实例上,而是在一个父类实例上,该子类实例是该父类实例的继承者。

缺点:子类实例不能直接调用父类实例的属性,只能通过直接调用父类实例的方法,或者通过this.__base__调用。