在过去两周左右, 一直在学习 kity, kityminder, nodejs, seajs 等一系列项目. 有些认识经过一段时间学习, 已经发生了
变化, 使得原有笔记过时. 兹为深入学习, 有必要回顾复习重新看一遍.
Kity 及 kityminder 的构建部分(grunt 相关), 这两天也看了下并请教了 baidu 的开发人员, 解决了原有不能构建的
问题, 现在可以顺利学习代码本身了. 该项目中原有带了几篇 md 格式的文档, 现在看也能懂更多些了. 文档的第一篇
第一章就是基础框架: Class 系统, 这在 kity 和 kityminder 中都被大量(重度)依赖使用, 因此很有必要仔细分析,
可惜的原工程的是 Kity Graphics OOP 文档未写, 故而只能先自己看代码学习.
在 kity 项目中, Class 系统实现在 src/core/class.js 中, 被 src/kity.js 入口模块引用(被最先引用的模块).
此模块提供 kity (也包括 kityminder) 的 OOP 支持.
首先定义了类 Class 作为所有 kity 类的基类. 这个类 (Class 类) 也被引出 (exports).
function Class() { // 构造函数为空, 主要提供成员函数. }
Class 类提供一组成员函数, 用于辅助建立一个类体系. 据我看这个类体系方便于继承, 混入(mixin), 得到类的
元信息(如类名, 父类)等.
// 下面给出 Class 的一些主要成员方法, 用伪代码的写法以方便理解. class Class { // 调用父类指定名称的函数, 参数 name 给出函数名称. // 使用示例: // Sub_Class.toString() = function() { // // 这里调用基类的 toString() 方法. 相当于 c#/java 的 super/base 语义. // return 'Hello ' + this.base('toString'); // } // 这个方法被多处使用. function base(name) { // 1. 为找到要调用的父类的函数, 需要提供必要的信息. // 得到调用此 base() 方法的调用者是谁, 用于知道子类是谁. var caller_method = arguments.callee.caller; // 2. 找到该方法所属类, 如上面例子得到的是 Sub_Class. // 注意: 这里要求每个(使用 base 的)方法都要有 __KityMethodClass 属性. // 一会我们细看下面在哪里设置了该属性. var method_class = caller_method.__KityMethodClass; // 3. 得到子类的父类(基类). 这里要求每个 class 要有属性 __KityBaseClass. var base_class = method_class.__KityBaseClass; // 4. 找到父类该名字的方法. 这里要求方法在 prototype 中(即不能是静态方法). var base_method = base_class.prototype[name]; // 5. 调用该基类方法(以 this 作为基类的 this), 这里未检查有没有该方法... base_method.apply(this, ...arguments...); } }
原有代码写得比较简单, 为了方便理解, 我改为一句一句的了. 这里重点是需要在(有需求的)方法上要有
__KityMethodClass 属性, 以及子类上有 __KityBaseClass 属性. 如果没有这些属性, 此方法即不能实现.
建立这些属性, 属于 Class 体系设计的重要范畴, 不得不仔细研究.
因此这里留下几个问题:
问题1: 这些 __KityXXX 属性是在什么时候, 什么地方产生的? (后面有回答)
问题2: 有没有别的方法可以使用此 base() 调用需求?
问题3: 为实现 base() 需求而付出的这些成本 是否值得?
===
先细研究一下 createClass 的部分:
/* 创建一个类. 给出参数 classname 类名, defines 类的定义. * 使用例子(原代码有数个例子, 我们合为一个为说明作用): * Class.createClass('Cat', { * base: Animal, 给出基类 * mixins: [Walkable], * constructor: function() --- 给出构造函数 * 在构造函数中可能调用基类/混入类: this.callBase(), this.callMixins() * toString: function() --- 其它函数. * }); */ export function createClass(classname, defines) { // 1. 得到基类, 这里语义是使用 Class 类做为基类, 如果未给出基类的话. // 实际由于 kity 都使用 createClass() 来创建类, 所以所有类都是 Class 子类. var BaseClass = defines.base || Class; // 2. 得到类的构造函数, 如果没有则设置一个缺省的. 缺省的以后再研究. var ctor = defines.constructor || default_ctor; // 3. 创建(定义)新类. var NewClass = inherit(ctor, BaseClass, classname); // 待续: ... 下面很多处理, 稍后研究... 现在先进入 inherite() 辅助函数学习. }
函数 inherite() 用于定义新类, 从指定基类 BaseClass 继承.
function inherite(ctor, BaseClass, classname) { // 1. 组装一个可 eval() 执行的字符串, 该字符串展开后形式如下: // 为方便, 假设类名 classname='Cat' class-define-string := function Cat(__inherite_flag) { if (__inherite_flag != KITY_INHERIT_FLAG) { KityClass.__KityConstructor.apply(this, arguments); } this.__KityClassName = KityClass.__KityClassName; } var KityClass = eval(class-define-string); // 执行该段语句. // 这里先仔细分析上述语义. // 1. 使用 __inherite_flag 标志, 当继承时此标志检测为 KITY_INHERITE_FLAG // 则不用调用 ctor 部分. 否则是正常的实例构造. // 2. 为每个实例对象 (this) 设置 __KityClassName 属性. // 待续... }
所以这部分代码回答了问题1的一部分, 即实例的 __KityClassName 属性在创建的构造函数中设置.
但是我又产生出一个新问题4: 为什么这里要使用 eval() 的方法来生成类?
下面以一个用上述函数定义出来的实际 kity.Point 类为例:
class kity.Point { // 如下静态的属性和方法相当于 Point 类自身的. static string __KityClassName = 'Point'; // 此类的名字. static string __KityMethodName = 'constructor'; // Point() 本身也是构造函数. static Class __KityBaseClass = Class; // 基类. static Class __KityMethodClass = Point; // Point() 方法属于 Point 类. static function __KityConstructor = point_ctor(x,y); // Point 的实际构造函数. // 以下相当于 Point.prototype 里的属性和方法. constructor = Point; // 实例的 constructor 属性即为 Point 类. function toString() ... // 此类的方法示例. // 其它方法和属性略... }
在浏览器 debugger/console 中查看, kity.Point 对象可看到如上结构.
当构造一个 kity.Point 的实例的时候, 如 var p = new kity.Point(3, 4); 然后观察 p, 则有:
p = { x: 3, y: 4, __KityClassName: 'Point' // 这个是eval的那个 Point 构造函数自动加上的..., 会不会困扰你? }
用 for 遍历一下 p 的属性:
> for (var i in p) if (p.hasOwnProperty(i)) console.log(i); > 结果为: x, y, __KityClassName
如果不限定 p.hasOwnProperty(i) 则会显示很多来自基类 Class 的方法, 这里略.
为了学习研究查找关于 mixin 的一个网页:
http://www.cnblogs.com/snandy/archive/2013/05/24/3086663.html
http://www.cnblogs.com/snandy/archive/2013/05/27/3097429.html
里面提到 Backbone 广泛使用 mixin. (有时间需要去看看)
===
下面接着看 inherit() 函数后面部分:
function inherit(ctor, BaseClass, classname) { // 前面用 eval() 创建类略去细节. var KityClass = eval(...); // 为 KityClass 类设置 static 属性 __KityConsturctor. 部分解答问题1. KityClass.__KityConstructor = ctor; // 为类设置原型. 这里要求总是有基类, 因为调用处检查若未给出基类, 则用 // 类 Class 作为基类. (问题5) // 这里使用了 KITY_INHERIT_FLAG 作为标志告诉基类, 是作为原型链使用. (问题6) var proto = KityClass.prototype = new BaseClass(KITY_INHERIT_FLAG); // 这里把基类的原型方法(及属性)都复制一份到新的类原型. (问题7:为什么?) var base_proto = BaseClass.prototype; for (var name in base_proto) { if (base_proto.hasOwnProperty(name) && name.not_include_Kity) proto[name] = base_proto[fname]; } // 设置新类原型属性 constructor. proto.constructor = KityClass; return KityClass; // 返回创建的新类. }
于是越来越多问题:
问题5: 是否可以不用 Class 作为缺省基类? 或者根本我们的新类就不要/没有基类?
问题6: KITY_INHERIT_FLAG 用对象是不是更好?
问题7: 为什么要将基类原型上的属性和方法复制到新类? 这样做有什么优点和缺点?
首先验证问题7本身是否正确, 即是否属性和方法是被复制到子类. 结果如下, 表明是被复制了.
for (var i in kity.Class.prototype) if (Class.prototype.hasOwnProperty(i)) console.log(i); 输出: base, mixin, pipe 等 Class 方法. for (var i in kity.Point.prototype) if (Point.prototype.hasOwnProperty(i)) console.log(i); 输出: base, mixin, pipe 等; 也包括 offset, valueOf, toString 等 Point 的方法.
现在 NewClass 创建出来了, 在函数 createClass() 中进行下一步:
export function createClass(classname, defines) { // 前面细节部分见上面 createClass(), 下面从这里开始. var NewClass = inherit(ctor, BaseClass, classname); // 创建新类时已处理了基类, 现在处理混入类(也有翻译为掺入). NewClass = mixin(NewClass, defines.mixins) // 待续... } // 找一个含 mixin 的类 kity.Curve 做例子. function mixin(NewClass, mixins) { if (mixins is not Array) return; // 需要是数组. // 为 NewClass 添加静态成员 __KityMixins, 下面向其添加很多东西. NewClass.__KityMixins = { constructor: [] }; // mixins 是一个数组, 循环每一个 mixin. for-each (mixin in mixins) { // 遍历 mixin.prototype 的自有属性(方法), 也排除掉以 __Kity 开头的. for-each-own-property (method in mixin.prototpye) { // 将 mixin 中的类方法添加到 NewClass 类中. // 在 __KityMixins 对象中也存一份. 细节是 ctor 特殊处理我们暂时略. NewClass.prototype[method] = NewClass.__KityMixins[method] = mixin.prototype[method]; } } }
实际浏览器查看 Curve 类:
class Curve { static object __KityMixins = { constructor: [PointContainer], // 被混入类的构造器数组. addItem: function, // 被混入的方法1. ... 许多许多方法 略... 但为什么这里也要放一份? } static prototype: { addItem: function, // 被混入的方法 ... 其它各种方法属性略... } }
表明 Curve 的确被混入了数个方法.
注意: mixin 也是将别的类的方法/属性复制到目标类(NewClass) 的 prototype 中.
接着看 createClass:
export function createClass(classname, defines) { // 细节见前面, 已完成创建及混入. var NewClass = inherit-and-mixin(...); // 这里要为 NewClass 添加一组 __Kity 开头的静态属性. // 设置此类的名字. 这里主要解答问题1. // ctor 中 this.__KityClassName = KityClass.__KityClassName; 值来自这里. NewClass.__KityClassName = ctor.__KityClassName = classname; // 设置基类引用属性, 这里 BaseClass 是类对象. NewClass.__KityBaseClass = ctor.__KityBaseClass = BaseClass; // 这里 NewClass 和 ctor 都是函数, 设置其 method name,class 属性. NewClass.__KityMethodName = ctor.__KityMethodName = 'constructor'; NewClass.__KityMethodClass = ctor.__KityMethodClass = NewClass; // 删掉不需要复制到原型链的属性. delete defines.base, .mixins, .constructor; // 最后一步了, 将 defines 中的属性方法复制到类 NewClass 的 prototype 上. NewClass = extendClass(NewClass, defines); return NewClass; }
这里对 NewClass (作为类, 也是一个函数) 设置了数个 __KityXXX 属性, 实际查看一下 kity.Curve 类, 我们能看到:
class Curve { static string __KityClassName = 'Curve'; static string __KityMethodName = 'constructor'; static object __KityMixins = { ... }; // 前面介绍过. static Class __KityBaseClass = Path; // Curve 的基类是 Path 类. static Class __KityMethodClass = Curve; // 此方法所属类是 Curve 即自己. static function __KityConstructor = ...; // 此类的实际 ctor() 函数. static prototype = { __KityClassName: 'Path', // 从父类 prototype 继承来的, 值不评价. constructor: Curve, // 其它略. } }
以下考察最后的 extendClass() 方法:
export function extendClass(BaseClass, extension) { if (extension is kity-defined-class) extension = extension.prototype; // 类向类扩展. // 遍历 extension 的属性和方法. 不能被变量名 method-name 误导了... // 并且非 __Kity 开头的, 非 constructor 名的... for-each-own-property (method-name in extension) { // 将这个方法复制到 BaseClass 的 prototype 上. var method = BaseClass.prototype[method-name] = extension[method-name]; // 这里要干某些 `魔法' 事情...? (问题8) method.__KityMethodClass = BaseClass; method.__KityMethodName = method-name; } }
于是这里我们产生了新问题8:
(1) 这里假设了 method 总是方法, 可是我们很可能给出的 extension 中含有属性, 假设有这么一个属性:
cache: {a:1, b:2}, 这里一处理就变成 cache: {__Kity... ...} 会不会很奇怪?
(2) 总是给 method 添加 __KityXXX 两个属性, 为了支持 base(), callBase() 等几个方法,
为了少数用途就要为每个函数添加东西, 成本会不会太高了? 还是要问, 是否有别的方法?
还是我们就心安理得于实现就行?
由于后面 kity, kityminder 的所有类(据现在看的代码) 都是用 createClass() 方法创建出来的, 符合 kity 类
体系模式的, 所以不得不仔细分析. 对这里理解透彻了, 也有助于学习其它部分的代码.
希望有时间再看看别的类库的创建类的方式, 对比研究也许更具启发意义吧.