作者:Shane Loo Li
历史:初版 2012-4-15 Sunday Shane Loo Li
修改首次初始化对象和类加载的执行顺序 2012-4-16 Monday Shane Loo Li
市面上流行了很多 JavaScript 面向对象的编程方法,其中不少都有好些问题。这里总结最正确的 JavaScript 面向对象编程模式。
对于类 Special 继承自类 Common 继承自类 Super 的情况,一个 Special 对象的创建,详细说来,应该经历以下步骤。
1 确定继承关系
1.1 读取 Special 的父类,发现是 Common
1.2 读取 Common 的父类,发现是 Super
1.3 读取 Super 的父类,发现没有了(隐形父类 Object)
2 加载类结构(如果没有加载的话。加载过的话则不需要重复加载)
2.1 加载 Super, Common, Special 类的类属性和类方法都有哪些
2.2 加载 Super 类对象具有哪些属性,哪些方法
2.3 加载 Common 类对象具有哪些属性,哪些方法
2.4 加载 Special 类对象具有哪些属性,哪些方法
2.5 给 Super, Common, Special 类的类属性分配空间
2.6 给 Super, Common, Special 类的类属性赋值
3 分配堆空间
3.1 分配 Super 的空间,包括 Super 所有成员的空间
3.2 分配 Common 的空间,包括 Common 所有成员的空间
3.3 分配 Special 的空间,包括 Special 所有成员的空间
4 初始化对象
4.1 调用 Super 的构造方法
4.2 调用 Common 的构造方法
4.3 调用 Special 的构造方法
那么,怎么用 JavaScript 合理并严格地依照标准对象构建顺序创建对象呢?
1 确定继承关系 。因为 JavaScript 没有变量类型绑定,所以应该能够允许多继承。其中第一个父类为主父类。
Super._super = [ ]; Common._super = [ Super ]; Special._super = [ Common ];
2 加载类结构
这里涉及到一个技术,熟悉 JavaScript 面向对象编程的人都会很了解,称为原型链(prototype chaining)。JavaScript 面向对象的类结构,其实是靠每一个类的原型对象来保存的。每生成一个对象,都是根据原型对象,制作一个与之具有同样成员,成员值也都相同的新对象。如果直接让一个类的原型对象等于一个其父类的对象,就相当于复制了父类原型的所有成员,放入了当前类的原型中。这些成员除了显示可见的意外,还包括用于 instanceof 的类名属性等一些系统成员变量,从而能够实现子类成员 instanceof 父类类名,得到 true 的结果——这种认定用的属性可以传递给更下一层的子类。
除了由原型链形成的主继承之外,因为 JavaScript 没有引用类型的概念,也就没有接口的概念。所以需要手动加载除主父类以外的其余父类成员。通过 for (var xxx in xxx) 的方式,可以取出其余父类原型中的成员,并用其建立当前类的类结构。这里需要注意,如果各种父类中有名称完全相同的方法,则会以最后加载进来的那个为子类的方法。这不得不说是 JavaScript 面向对象多重继承的一个遗憾。
Super._loadclass = function() { if (Super._initialized) return; // 继承父类的类结构。因为没有父类,所以继承父类类结构的语句可以去掉 // 创建自身独有类结构 Super.prototype.field01 = null; Super.prototype.field02 = null; Super.prototype.method01 = function() { // 方法体,其中可以使用 this 关键字 }; Super.prototype.method02 = function() { // 方法体,其中可以使用 this 关键字 }; Super._initialized = true; };
Common._loadclass = function() { if (Common._initialized) return; // 继承父类的类结构 if (Common._super.length != 0) { Common.prototype = new Common._super[0](); } for (var index in Common._super) // 为防止下标不连续,使用此种循环 { var superClass = Common._super[index]; if (!superClass._initialized) { superClass._loadclass(); } // 如果父类尚未加载,则先加载父类 for (var key in superClass.prototype) { if (key.charAt(0) == "_" || key == "constructor") { continue; } // 系统成员和构造方法不继承给子类 var value = superClass.prototype[key]; if (typeof value == "function") Common.prototype[key] = value; else Common.prototype[key] = null; } } // 创建自身独有类结构 Common.prototype.field03 = null; Common.prototype.method03 = function() { // 方法体,其中可以使用 this 关键字 }; Common._initialized = true; }
Special._loadclass = function() { if (Special._initialized) return; // 继承父类的类结构 if (Special._super.length != 0) Special.prototype = new Special._super[0](); for (var index in Special._super) // 为防止下标不连续,使用此种循环 { var superClass = Special._super[index]; if (!superClass._initialized) { superClass._loadclass(); } // 如果父类尚未加载,则先加载父类 for (var key in superClass.prototype) { if (key.charAt(0) == "_" || key == "constructor") { continue; } // 系统成员和构造方法不继承给子类 var value = superClass.prototype[key]; if (typeof value == "function") Special.prototype[key] = value; else Special.prototype[key] = null; } } // 创建自身独有类结构 Special.prototype.field04 = null; Special.prototype.method02 = function() // 覆盖父类的方法 { Super.prototype.method02(); // 调用被覆盖的父类方法 // 方法体,其中可以使用 this 关键字 }; Special.prototype.method04 = function() { // 方法体,其中可以使用 this 关键字 }; Special._initialized = true; }
_loadclass 方法可以放在构造方法里边执行,以便首次执行构造方法生成对象之前,现场初次加载类结构。当然,也可以在声明构造方法之后统一执行,以便在浏览器获取 .js 之后即可完成加载。后边的例子会按照第一种方式,也就是首次生成对象时加载类结构来编写,以表达作者我对爪哇语言的不经意间的熟悉。
另外我们可以看到每个类进行类加载时,加载继承结构的代码都完全一样。所以我们可以提取出一个顶层公共方法以方便类加载。在加载一个类之前需要设定其父类数组。
/** * 加载父类类结构 * 本方法用以加载指定类的所有继承结构。要求传入的类已经设定了 _super ,且是一个 * 数组类型对象,里边的元素都是一个个类。 * @param classObj 父类的类对象,即其构造方法自身,是一个函数类型的对象。一 * 般来说,作为构造方法的函数,应该以大写字母开头,以示这是一个“类名”。 */ var _loadSuperClassStruct = function(classObj) { if (!classObj || typeof classObj != "function" || !classObj._super) { return false; } // 如果已经被初始化,则认为父类加载已经成功完成 if (classObj._initialized) { return true; } // 继承父类的类结构 if (classObj._super.length != 0) { classObj.prototype = new classObj._super[0](); } for (var index in classObj._super) // 以防 index 不连续 { var superClass = classObj._super[index]; if (!superClass._initialized) { try { superClass._loadclass(); // 如果父类尚未加载,则先加载父类 } catch (ex) { continue; } // 如果父类无法加载,则跳过继承这个父类 } for (var key in superClass.prototype) { if (key.charAt(0) == "_" || key == "constructor") { continue; } // 系统成员和构造方法不继承给子类 var value = superClass.prototype[key]; if (typeof value == "function") { classObj.prototype[key] = value; } else { classObj.prototype[key] = null; } } } return true; };于是,加载一个类结构的方法就可以如下
ClassA._loadclass = function() { if (ClassA._initialized) return; // 继承父类的类结构 if (!_loadSuperClassStruct(ClassA)) { throw new Error("Can't load super class of ClassA."); } // 创建自身独有类结构 ClassA.prototype.field = null; ClassA.prototype.method = function() { // 方法体,其中可以使用 this 关键字 }; // 确认类加载完成 ClassA._initialized = true; }
3 分配堆空间
按照脚本语言标准,分配堆空间,也就是在堆内存中开辟对象的空间,是由 new 关键字构成的。
new Super(); new Common(); new Special();
4 初始化对象
构造方法提供初始化对象的功能。如果尚未加载类,则应先加载类。
这里边涉及二个技术,称为对象冒充(object masquerading)和混合工厂方式对象构造法。
对象冒充是利用 JavaScript 其中 ECMAScript 部分,即脚本标准语言部分中反射的机制,来在一种对象的构造方法中执行别的对象的构造方法。因为 this. 字段所指的内容发生了变化,所以被调用的构造方法中的语句都会变为构造当前对象。相关内容可参看 function 类对象的 call() 方法和 apply() 方法。
混合工厂方式基于 new 不可重叠原理来实现。对于符合 ECMAScript 标准的语言来说,new 的创建空间作用,应该在构造方法执行之前先行生效。对于 JavaScript 来说,就意味着当第一次生成新对象执行 new 的时候,对象类还没有加载,那么 new 出来的就是一个极其不完整的东西,该有的成员很可能都不会存在。所以在构造方法里边再行 new 一个对象,并将其作为返回值;此时因为构造方法内部 new 的存在,调用构造方法之前的那个 new 就会被无效化。于是看起来是直接新建了一个对象,其实是在构造方法内部,加载类完毕之后再新建的对象。这样就避免了初次建立对象无法正确获得类结构的问题。只是我不知道为什么这种多次 new 只有一个能生效的特性会被称为“混合工厂方式对象构造法”。可能是说这种把待生成对象当做返回值的方法应被称为工厂方法,而其本身又是构造方法,所以叫“混合工厂方法”吧。
function Super(field01Value) { // 先加载类 if (!Super._initialized) { Super._loadclass(); return new Super(field01Value); } // 初始化对象各变量 this.field01 = field01Value; this.field02 = "super value 02"; } function Common() { // 先加载类 if (!Common._initialized) { Common._loadclass(); return new Common(); } // 调用父类构造方法 Super.call(this, "common value 01"); // 初始化对象各变量 this.field03 = "common value 03"; } function Special() { // 先加载类 if (!Special._initialized) { Special._loadclass(); return new Special(); } // 调用父类构造方法 Common.call(this, ""); // 初始化对象各变量 this.field04 = "special value 04"; this.field01 = "special value 01"; // 修改父类已经赋值过的属性值 }以上,不知道这个例子是不是妥帖。通过这种标准化的类加载方式,我们可以将严格的面向对象程序设计方案引入 JavaScript 。额外的,这种严格过程的对象生成方式,可以方便程序员生成新对象之后控制内存防止泄露。也能够避免方法体重复加载带来的内存浪费。如果将相关内容做成通用的 .js 文件,则并不会为对象化编程带来什么额外编码负担。
接下来用 一个例子最后总结一下 JavaScript 严格面向对象的编程方法。
/** * 加载父类类结构 * 本方法用以加载指定类的所有继承结构。要求传入的类已经设定了 _super ,且是一个 * 数组类型对象,里边的元素都是一个个类。 * @param classObj 父类的类对象,即其构造方法自身,是一个函数类型的对象。一 * 般来说,作为构造方法的函数,应该以大写字母开头,以示这是一个“类名”。 */ var _loadSuperClassStruct = function(classObj) { if (!classObj || typeof classObj != "function" || !classObj._super) { return false; } // 如果已经被初始化,则认为父类加载已经成功完成 if (classObj._initialized) { return true; } // 继承父类的类结构 if (classObj._super.length != 0) { classObj.prototype = new classObj._super[0](); } for (var index in classObj._super) // 以防 index 不连续 { var superClass = classObj._super[index]; if (!superClass._initialized) { try { superClass._loadclass(); // 如果父类尚未加载,则先加载父类 } catch (ex) { continue; } // 如果父类无法加载,则跳过继承这个父类 } for (var key in superClass.prototype) { if (key.charAt(0) == "_" || key == "constructor") { continue; } // 系统成员和构造方法不继承给子类 var value = superClass.prototype[key]; if (typeof value == "function") { classObj.prototype[key] = value; } else { classObj.prototype[key] = null; } } } return true; }; // ---------- ---------- Animal 的声明段 ---------- ---------- /** * 声明继承关系:Animal 类的父类——没有父类。 */ Animal._super = [ ]; /** * Animal 类的类加载方法。 */ Animal._loadclass = function() { if (Animal._initialized) { return; } // 继承父类的类结构 if (!_loadSuperClassStruct(Animal)) { throw new Error("Can't load super class of Animal."); } // 创建自身独有类结构 Animal.prototype.name = null; Animal.prototype.health = null; Animal.prototype.run = function() { if (this.getHealth()) { alert(this.name + " is running..."); } else { alert(this.name + " is walking..."); } }; Animal.prototype.getHealth = function() { return this.health; } Animal._initialized = true; }; /** * Animal 类对象的构造方法 */ function Animal(nameValue) { // 先加载类 if (!Animal._initialized) { Animal._loadclass(); return new Animal(nameValue); } // 初始化对象各变量 this.name = nameValue; } // ---------- ---------- Bird 的声明段 ---------- ---------- /** * 声明继承关系:Bird 类的父类是 Animal。 */ Bird._super = [ Animal ]; /** * Bird 类的类加载方法。 */ Bird._loadclass = function() { if (Bird._initialized) return; // 继承父类的类结构 if (!_loadSuperClassStruct(Bird)) { throw new Error("Can't load super class of Bird."); } // 创建自身独有类结构 Bird.prototype.fly = function() { if (this.getHealth()) { alert(this.name + " is flying..."); } else { alert("Sorry, it can't fly. Please wait for some day."); } }; Bird._initialized = true; }; /** * Bird 类对象的构造方法 */ function Bird(nameValue) { // 先加载类 if (!Bird._initialized) { Bird._loadclass(); return new Bird(nameValue); } // 初始化对象各变量 Animal.call(this, nameValue); } // ---------- ---------- HomeAnimal 的声明段 ---------- ---------- /** * 声明继承关系:HomeAnimal 类的父类是 Animal。 */ HomeAnimal._super = [ Animal ]; /** * HomeAnimal 类的类加载方法。 */ HomeAnimal._loadclass = function() { if (HomeAnimal._initialized) return; // 继承父类的类结构 if (!_loadSuperClassStruct(HomeAnimal)) { throw new Error("Can't load super class of HomeAnimal."); } // 创建自身独有类结构 HomeAnimal.prototype.getHealth = function() { return true; }; HomeAnimal._initialized = true; }; /** * HomeAnimal 类对象的构造方法 */ function HomeAnimal(nameValue) { // 先加载类 if (!HomeAnimal._initialized) { HomeAnimal._loadclass(); return new HomeAnimal(nameValue); } // 初始化对象各变量 Animal.call(this, nameValue); } // ---------- ---------- Duck 的声明段 ---------- ---------- /** * 声明继承关系:Duck 有二个父类,分别是 Bird 和 HomeAnimal ,其中 Bird 是主父类 */ Duck._super = [ Bird, HomeAnimal ]; /** * Duck 类的类加载方法。 */ Duck._loadclass = function() { if (Duck._initialized) return; // 继承父类的类结构 if (!_loadSuperClassStruct(Duck)) { throw new Error("Can't load super class of Duck."); } // 创建自身独有类结构 Duck.prototype.fly = function() { alert("Duck can't fly."); }; Duck._initialized = true; }; /** * Duck 类对象的构造方法 */ function Duck() { // 先加载类 if (!Duck._initialized) { Duck._loadclass(); return new Duck(); } // 初始化对象各变量 Bird.call(this, "duck"); } // ---------- ---------- 测试程序段 ---------- ---------- var duckObj = new Duck(); // 方法调用 duckObj.run(); // 继承自父类的方法。方法中还会调用 HomeAnimal 覆盖 Animal 的方法:getHealth() duckObj.fly(); // 覆盖掉 Bird 类的方法。 // 对象类型认定 alert(duckObj instanceof Duck); alert(duckObj instanceof Bird); alert(duckObj instanceof HomeAnimal); alert(duckObj instanceof Animal); // 类结构动态改变效果确认 Duck.prototype.testMethod = function() { alert("OK"); } duckObj.testMethod();以上例子是我昨天写的,可以在浏览器中直接使用以检测效果。
相关概念参考资料: http://www.w3school.com.cn/js/index_pro.asp
本文还发表在我的技术博客
51CTO : http://shanelooli.blog.51cto.com/5523233/1087677