再学习 Kity 笔记(五) 初步

在过去两周左右, 一直在学习 kity, kityminder, nodejs, seajs 等一系列项目. 有些认识经过一段时间学习, 已经发生了
变化, 使得原有笔记过时. 兹为深入学习, 有必要回顾复习重新看一遍.

Kity 及 kityminder 的构建部分(grunt 相关), 这两天也看了下并请教了 baidu 的开发人员, 解决了原有不能构建的
问题, 现在可以顺利学习代码本身了. 该项目中原有带了几篇 md 格式的文档, 现在看也能懂更多些了. 文档的第一篇
第一章就是基础框架: Class 系统, 这在 kity 和 kityminder 中都被大量(重度)依赖使用, 因此很有必要仔细分析,
可惜的原工程的是 Kity Graphics OOP 文档未写, 故而只能先自己看代码学习.

在 kity 项目中, Class 系统实现在 src/core/class.js 中, 被 src/kity.js 入口模块引用(被最先引用的模块).

Class 系统

此模块提供 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 类
体系模式的, 所以不得不仔细分析. 对这里理解透彻了, 也有助于学习其它部分的代码.

希望有时间再看看别的类库的创建类的方式, 对比研究也许更具启发意义吧.

 

你可能感兴趣的:(再学习 Kity 笔记(五) 初步)