初看原型
JS的所有函数都有一个prototype属性,这个prototype属性本身又是一个object类型的对象。
prototype提供了一群同类对象共享属性和方法的机制。
将一个基类的实例作为子类的原型对象,原型继承
Employee.prototype = new Person();
JS通过简单的重写机制实现对象的“多态”性,与静态对象语言的虚函数和重载概念不谋而合。
可以通过给原型对象动态添加新的属性和方法,从而动态地扩展基类的功能特性。
在原型模型中,为了实现类继承,必须首先将子类构造函数的prototype设置为一个父类的对象实例。创建这个父类实例的目的就是为了构成原型链,以起到共享上层原型方法的作用。但创建这个实例对象时,上层构造函数也会给它设置对象成员,这些对象成员对于继承来说是没有意义的。虽然我们也没有给构造函数传递参数,但确实创建了若干没有用的成员,尽管其值是undefined,这也是一种浪费。
原型扩展
给内置的对象的构造函数添加新的方法和属性,从而增强JS的功能。
String.prototype.trim = function() { /* ... */ };
原型真谛
JS构造对象的过程可以分三步:第一步,创建一个空对象;第二步,将这对象内置的原型对象设置为构造函数prototype引用的原型对象;第三步,将该对象作为this参数调用构造函数,完成成员设置等初始化工作。
对象建立后,对象上的任何访问和操作都只与对象自身及其原型链上的那串对象相关,与构造函数基本扯不上关系了。换句话说,构造函数只是在创建对象时起到介绍原型对象和初始化对象两个作用。
思考:
那么我们能否自己定义一个对象来当做原型,并在这个原型上描述类,然后将这个原型设置给新创建的对象,将其当作对象的类呢?我们又能否将这个原型中的一个方法当作构造函数,去初始化新创建的对象呢?例如,我们定义这样一个原型对象:
var People = { // 定义一个对象当作原型 create: function(name, age) { // 这个当成构造函数 this.name = name; this.age = age; }, sayName: function() { // 定义方法 return this.name; }, howOld: function() { return this.age; } };
利用通用函数建立指定原型类的对象
/* * 通过函数创建新对象并返回 * @param {Object} aClass 指定的原型类 * @param {Array} aParams 参数数组 */ function New(aClass, aParams) { // 创建通用函数 function _New() { // 定义临时的中转函数壳 // 调用原型中定义的构造函数,中转构造逻辑和传递参数 aClass.create.apply(this, aParams); } _New.prototype = aClass; // 准备中转原型对象 return new _New(); } 修复版 /* * 通过函数创建新对象并返回 * @param {Object} aClass 指定的原型类 * @param {Array} aParams 参数数组 */ function New(aClass, aParams) { function _New() { // 定义临时的中转函数壳 this.Type = aClass; // 给每个对象约定Type属性,据此可以访问到对象所属的类 // 调用原型中定义的构造函数,中转构造逻辑和传递参数 // create方法是约定的构造函数 if(aClass.create) aClass.create.apply(this, aParams); } _New.prototype = aClass; // 准备中转原型对象 return new _New(); }
通用函数New()就是一个语法甘露,不但中转原型对象,还中转了构造函数逻辑和构造参数。
有趣的是,每次创建完对象退出New函数作用域时,临时的_New函数对象会被自动释放。
我们还需要多一些语法甘露,实现类层次及其继承关系。
// 简洁而优雅地书写类层次及其继承关系 var object = { // 基本类,用于定义最基本的方法 isA: function(aBase) { // 判断类与类之间及类与对象的之间的关系 var self = this; while(self) { if(self ==aBase) return true; else self = self.Type; } return false; } }; // 创建类的函数,用于声明类及继承关系 function Class(aBaseClass, aClassDefine) { // 创建类的临时函数壳 function _class() { this.Type = aBaseClass; // 给每个类约定一个Type属性,引用其继承关系 // 复制类的全部定义到当前创建的类 for(var prop in aClassDefine) { this[prop] = aClassDefine[prop]; } } _class.prototype = aBaseClass; return new _class(); }
令人高兴的是,受这些甘露滋养的JS程序效率会更高效。因为其原型对象里既没有了毫无用处的那些对象级的成员,而且还不存在constructor属性体,少了与构造函数之间的牵连,但依旧保持了方法的共享性。
甘露模型
使用Class()甘露,我们已经可以用非常优雅的格式定义一个类。
// 定义Student类,继承People // Student其实是一个对象,是模拟成的类 // 可以通过New函数创建Student对象 var Student = Class(People, { // 派生自People类 create: function(name, age, grade) { // 调用超类的构造函数 People.create.call(this, name, age); this.grade = grade; }, getGrade: function() { return '我的成绩是' + this.grade; } });
Class()语法甘露实际上是构造一个原型,并将这个原型挂在了相应的原型链上。它返回的是一个对象而不是函数。如果让Class()返回一个函数,不就可以用new Student()这种方式来创建对象吗?而且,我们可以为这个返回函数创建一个继承至相关原型链的原型对象,并设置到函数的prototype属性。这样,我们用new方式创建这个类函数的对象时,就自然地继承该类的原型了。
// 定义类的语法甘露:Class() // 最后一个参数是JSON表示的类定义 // 如果参数的数量大于1,则第一个参数是基类 // 第一个参数和最后一个参数之间,可以表示类实现的接口 // 返回一个类,类是一个构造函数 function Class() { var aDefind = arguments[arguments.length-1]; // 类定义 if( !aDefind ) return; // 确定基类,默认是object基本类 // aBase 是一个构造函数,会继承它的原型对象 var aBase = arguments.length > 1? arguments[0] : object; function prototype_() {} // 临时函数, 用于挂接原型链 prototype_.prototype = aBase.prototype; // 准备传递prototype aPrototype = new prototype_(); // 构造函数的prototype // 复制类定义到当前类的原型prototype上 for(var member in aDefind) { if(member != 'create') // 不复制构造函数create aPrototype[member] = aDefind[member]; } var aType; // 存放构造函数的引用 if(aDefind.create) aType = aDefind.create; // 类型即为该构造函数 else aType = function() { // 如果未定义create(), 使用默认构造函数 this.base.apply(this, arguments); // 调用基类的构造函数 } aType.prototype = aPrototype; // 给构造函数的原型属性赋值 aType.Base = aBase; // 设置类型关系(默认是object) aType.prototype.Type = aType; // 为本类对象扩展一个Type属性,用于判断对象所属类 return aType; // 返回构造函数作为类 } // 根类object的定义 function object() { /* object 基本类 */ }; object.prototype.isA = function(aType) { var self = this.type; // 调用isA()的当前类 while(self) { if(self == aType) return true; self = self.aBase; // 检查基类是否相等 } return false; }; object.prototype.base = function() { // 用于调用基类的构造函数 var Caller = object.prototype.base.caller; // 当前构造函数create() Caller && Caller.Base && Caller.Base.apply(this, arguments); };
使用Class()函数来定义一个类,实际上就是为创建对象准备了一个构造函数,而该构造函数的prototype已经初始化为方法表,并可继承上层类的方法表。这样当用new操作符创建一个对象时,也就很自然地将此构造函数的原型链传递给了新构造的对象。
Class()函数中的小问题
不支持不可枚举的内置方法如toString();object根类的base()方法用到了函数的caller属性,以此判断构造函数的层次。
1. 解决不能覆写不可枚举属性的问题并非难事。既然这些属性特殊,就可以对其进行特殊处理。我们在for-in复制完可枚举属性后,加上下面的处理语句:
if(aDefind.toString !== Object.prototype.toString) {
aPrototype['toString'] = aDefind.toString;
}
2. base()方法之所以要使用只身的caller属性,就是为了确定当前构造函数的层次,从而可以知道该调用上层的构造函数。
实际上,第一层构造函数调用this.base()时,我们可以通过this.Type属性知道一层构造函数,而this.Type.Base就是第二层构造函数。只是,第二层构造函数又会调用this.base(),其本来是想调用第三层的构造函数,但再次进入base()函数时,就无法知晓构造函数的层次了。
如果我们在第一层构造函数调用进入this.base()时,先改变this.base本身,让其在下次被调用时能掉到第3层构造函数。完成这个变身动作之后再调第2层构造函数,而第2层构造函数再调用this.base()时就能调用到第3层构造函数了。这样只要我们在每次的base()调用中都完成一个自我的变身动作,就可以按正确的顺序完成对构造函数的调用。
object.prototype.base = function() { // 用于调用基类的构造函数 var Base = this.type.Base; // 第2层构造函数 if(!Base.Base) { // 如果基类没有基类 Base.apply(this, arguments); // 就直接调用基类的构造函数 }else { // 如果基类上面还有基类 this.base = makeBase_(Base); // 先覆盖this.base,返回一个函数 Base.apply(this, arguments); // 然后调用基类构造函数,其调用this.base delete this.base; } function makeBase_(Type) { // 包装基类构造函数 var Base = Type.Base; // 第3层构造函数 if (!Base.Base) return Base; // 不存在基类,直接返回 return function() { // 包装为引用临时变量Base的闭包函数 this.base = makeBase_(Base);// 先覆写this.base Base.apply(this, arguments);// 再调用基类的构造函数 }; } };
makeBase_()函数,如果基类还有基类,它就返回一个闭包函数。下次this.base()被构造函数调用时,即调用的是这个闭包函数。但这个闭包函数又可能会调用makeBase_()形成另一个闭包函数,直到基类再无基类。
重写后的完美甘露模型代码