• JavaScript面向对象的支持(4)


    ================================================================================
    Qomolangma OpenProject v0.9


    类别    :Rich Web Client
    关键词  :JS OOP,JS Framwork, Rich Web Client,RIA,Web Component,
              DOM,DTHML,CSS,JavaScript,JScript

    项目发起:aimingoo (aim@263.net)
    项目团队:aimingoo, leon(pfzhou@gmail.com)
    有贡献者:JingYu(zjy@cnpack.org)
    ================================================================================

    八、JavaScript面向对象的支持
    ~~~~~~~~~~~~~~~~~~
    (续)

    3. 构造、析构与原型问题
    --------
     我们已经知道一个对象是需要通过构造器函数来产生的。我们先记住几点:
       - 构造器是一个普通的函数
       - 原型是一个对象实例
       - 构造器有原型属性,对象实例没有
       - (如果正常地实现继承模型,)对象实例的constructor属性指向构造器
       - 从三、四条推出:obj.constructor.prototype指向该对象的原

     好,我们接下来分析一个例子,来说明JavaScript的“继承原型”声明,以
    及构造过程。
    //---------------------------------------------------------
    // 理解原型、构造、继承的示例
    //---------------------------------------------------------
    function MyObject() {
      this.v1 = 'abc';
    }

    function MyObject2() {
      this.v2 = 'def';
    }
    MyObject2.prototype = new MyObject();

    var obj1 = new MyObject();
    var obj2 = new MyObject2();

     1). new()关键字的形式化代码
     ------
     我们先来看“obj1 = new MyObject()”这行代码中的这个new关键字。

    new关键字用于产生一个新的实例(说到这里补充一下,我习惯于把保留字叫关键
    字。另外,在JavaScript中new关键字同时也是一个运算符),这个实例的缺省属性
    中,(至少)会执有构造器函数的原型属性(prototype)的一个引用(在ECMA Javascript
    规范中,对象的这个属性名定义为__proto__)。

    每一个函数,无论它是否用作构造器,都会有一个独一无二的原型对象(prototype)。
    对于JavaScript“内置对象的构造器”来说,它指向内部的一个原型。缺省时JavaScript
    构造出一个“空的初始对象实例(不是null)”并使原型引用指向它。然而如果你给函
    数的这个prototype赋一个新的对象,那么新的对象实例将执有它的一个引用。

    接下来,构造过程将调用MyObject()来完成初始化。——注意,这里只是“初始
    化”。

    为了清楚地解释这个过程,我用代码形式化地描述一下这个过程:
    //---------------------------------------------------------
    // new()关键字的形式化代码
    //---------------------------------------------------------
    function new(aFunction) {
      // 基本对象实例
      var _this = {};

      // 原型引用
      var _proto= aFunction.prototype;

    /* if compat ECMA Script
      _this.__proto__ = _proto;
    */

      // 为存取原型中的属性添加(内部的)getter
      _this._js_GetAttributes= function(name) {
        if (_existAttribute.call(this, name))
          return this[name]
        else if (_js_LookupProperty.call(_proto, name))
          retrun OBJ_GET_ATTRIBUTES.call(_proto, name)
        else
          return undefined;
      }

      // 为存取原型中的属性添加(内部的)setter
      _this._js_GetAttributes = function(name, value) {
        if (_existAttribute.call(this, name))
          this[name] = value
        else if (OBJ_GET_ATTRIBUTES.call(_proto, name) !== value) {
          this[name] = value    // 创建当前实例的新成员
        }
      }

      // 调用构造函数完成初始化, (如果有,)传入args
      aFunction.call(_this);

      // 返回对象
      return _this;
    }

    所以我们看到以下两点:
      - 构造函数(aFunction)本身只是对传入的this实例做“初始化”处理,而
        不是构造一个对象实例。
      - 构造的过程实际发生在new()关键字/运算符的内部。

    而且,构造函数(aFunction)本身并不需要操作prototype,也不需要回传this。


     2). 由用户代码维护的原型(prototype)链
     ------
     接下来我们更深入的讨论原型链与构造过程的问题。这就是:
      - 原型链是用户代码创建的,new()关键字并不协助维护原型链

    以Delphi代码为例,我们在声明继承关系的时候,可以用这样的代码:
    //---------------------------------------------------------
    // delphi中使用的“类”类型声明
    //---------------------------------------------------------
    type
      TAnimal = class(TObject); // 动物
      TMammal = class(TAnimal); // 哺乳动物
      TCanine = class(TMammal); // 犬科的哺乳动物
      TDog = class(TCanine);    // 狗

    这时,Delphi的编译器会通过编译技术来维护一个继承关系链表。我们可以通
    过类似以下的代码来查询这个链表:
    //---------------------------------------------------------
    // delphi中使用继关系链表的关键代码
    //---------------------------------------------------------
    function isAnimal(obj: TObject): boolean;
    begin
      Result := obj is TAnimal;
    end;

    var
      dog := TDog;

    // ...
    dog := TDog.Create();
    writeln(isAnimal(dog));

    可以看到,在Delphi的用户代码中,不需要直接继护继承关系的链表。这是因
    为Delphi是强类型语言,在处理用class()关键字声明类型时,delphi的编译器
    已经为用户构造了这个继承关系链。——注意,这个过程是声明,而不是执行
    代码。

    而在JavaScript中,如果需要获知对象“是否是某个基类的子类对象”,那么
    你需要手工的来维护(与delphi这个例子类似的)一个链表。当然,这个链表不
    叫类型继承树,而叫“(对象的)原型链表”。——在JS中,没有“类”类型。

    参考前面的JS和Delphi代码,一个类同的例子是这样:
    //---------------------------------------------------------
    // JS中“原型链表”的关键代码
    //---------------------------------------------------------
    // 1. 构造器
    function Animal() {};
    function Mammal() {};
    function Canine() {};
    function Dog() {};

    // 2. 原型链表
    Mammal.prototype = new Animal();
    Canine.prototype = new Mammal();
    Dog.prototype = new Canine();

    // 3. 示例函数
    function isAnimal(obj) {
      return obj instanceof Animal;
    }

    var
      dog = new Dog();
    document.writeln(isAnimal(dog));

    可以看到,在JS的用户代码中,“原型链表”的构建方法是一行代码:
      "当前类的构造器函数".prototype = "直接父类的实例"

    这与Delphi一类的语言不同:维护原型链的实质是在执行代码,而非声明。

    那么,“是执行而非声明”到底有什么意义呢?

    JavaScript是会有编译过程的。这个过程主要处理的是“语法检错”、“语
    法声明”和“条件编译指令”。而这里的“语法声明”,主要处理的就是函
    数声明。——这也是我说“函数是第一类的,而对象不是”的一个原因。

    如下例:
    //---------------------------------------------------------
    // 函数声明与执行语句的关系(firefox 兼容)
    //---------------------------------------------------------
    // 1. 输出1234
    testFoo(1234);

    // 2. 尝试输出obj1
    // 3. 尝试输出obj2
    testFoo(obj1);
    try {
      testFoo(obj2);
    }
    catch(e) {
      document.writeln('Exception: ', e.description, '<BR>');
    }

    // 声明testFoo()
    function testFoo(v) {
      document.writeln(v, '<BR>');
    }

    //  声明object
    var obj1 = {};
    obj2 = {
      toString: function() {return 'hi, object.'}
    }

    // 4. 输出obj1
    // 5. 输出obj2
    testFoo(obj1);
    testFoo(obj2);

    这个示例代码在JS环境中执行的结果是:
    ------------------------------------
      1234
      undefined
      Exception: 'obj2' 未定义
      [object Object]
      hi, obj
    ------------------------------------

    问题是,testFoo()是在它被声明之前被执行的;而同样用“直接声明”的
    形式定义的object变量,却不能在声明之前引用。——例子中,第二、三
    个输入是不正确的。

    函数可以在声明之前引用,而其它类型的数值必须在声明之后才能被使用。
    这说明“声明”与“执行期引用”在JavaScript中是两个过程。

    另外我们也可以发现,使用"var"来声明的时候,编译器会先确认有该变量
    存在,但变量的值会是“undefined”。——因此“testFoo(obj1)”不会发
    生异常。但是,只有等到关于obj1的赋值语句被执行过,才会有正常的输出。
    请对照第二、三与第四、五行输出的差异。

    由于JavaScript对原型链的维护是“执行”而不是“声明”,这说明“原型
    链是由用户代码来维护的,而不是编译器维护的。

    由这个推论,我们来看下面这个例子:
    //---------------------------------------------------------
    // 示例:错误的原型链
    //---------------------------------------------------------
    // 1. 构造器
    function Animal() {}; // 动物
    function Mammal() {}; // 哺乳动物
    function Canine() {}; // 犬科的哺乳动物

    // 2. 构造原型链
    var instance = new Mammal();
    Mammal.prototype = new Animal();
    Canine.prototype = instance;

    // 3. 测试输出
    var obj = new Canine();
    document.writeln(obj instanceof Animal);

    这个输出结果,使我们看到一个错误的原型链导致的结果“犬科的哺乳动
    物‘不是’一种动物”。

    根源在于“2. 构造原型链”下面的几行代码是解释执行的,而不是象var和
    function那样是“声明”并在编译期被理解的。解决问题的方法是修改那三
    行代码,使得它的“执行过程”符合逻辑:
    //---------------------------------------------------------
    // 上例的修正代码(部分)
    //---------------------------------------------------------
    // 2. 构造原型链
    Mammal.prototype = new Animal();
    var instance = new Mammal();
    Canine.prototype = instance;


     3). 原型实例是如何被构造过程使用的
     ------
     仍以Delphi为例。构造过程中,delphi中会首先创建一个指定实例大小的
    “空的对象”,然后逐一给属性赋值,以及调用构造过程中的方法、触发事
    件等。

    JavaScript中的new()关键字中隐含的构造过程,与Delphi的构造过程并不完全一致。但
    在构造器函数中发生的行为却与上述的类似:
    //---------------------------------------------------------
    // JS中的构造过程(形式代码)
    //---------------------------------------------------------
    function MyObject2() {
      this.prop = 3;
      this.method = a_method_function;

      if (you_want) {
        this.method();
        this.fire_OnCreate();
      }
    }
    MyObject2.prototype = new MyObject(); // MyObject()的声明略

    var obj = new MyObject2();

    如果以单个类为参考对象的,这个构造过程中JavaScript可以拥有与Delphi
    一样丰富的行为。然而,由于Delphi中的构造过程是“动态的”,因此事实上
    Delphi还会调用父类(MyObject)的构造过程,以及触发父类的OnCreate()事件。

    JavaScript没有这样的特性。父类的构造过程仅仅发生在为原型(prototype
    属性)赋值的那一行代码上。其后,无论有多少个new MyObject2()发生,
    MyObject()这个构造器都不会被使用。——这也意味着:
      - 构造过程中,原型对象是一次性生成的;新对象只持有这个原型实例的引用
        (并用“写复制”的机制来存取其属性),而并不再调用原型的构造器。

    由于不再调用父类的构造器,因此Delphi中的一些特性无法在JavaScript中实现。
    这主要影响到构造阶段的一些事件和行为。——无法把一些“对象构造过程中”
    的代码写到父类的构造器中。因为无论子类构造多少次,这次对象的构造过程根
    本不会激活父类构造器中的代码。

    JavaScript中属性的存取是动态的,因为对象存取父类属性依赖于原型链表,构造
    过程却是静态的,并不访问父类的构造器;而在Delphi等一些编译型语言中,(不使
    用读写器的)属性的存取是静态的,而对象的构造过程则动态地调用父类的构造函数。
    所以再一次请大家看清楚new()关键字的形式代码中的这一行:
    //---------------------------------------------------------
    // new()关键字的形式化代码
    //---------------------------------------------------------
    function new(aFunction) {
      // 原型引用
      var _proto= aFunction.prototype;

      // ...
    }

    这个过程中,JavaScript做的是“get a prototype_Ref”,而Delphi等其它语言做
    的是“Inherited Create()”。

    (本节未完待续...)

  • 相关阅读:
    JavaScript初学者应注意的七个细节
    8个高质量图标的最佳搜索引擎
    Adobe CS5 For Mac综合贴(2011/01/22更新)
    http://apps.hi.baidu.com/share/detail/18571966
    不要使用@import
    【leetcode】Search in Rotated Sorted Array
    【leetcode】Excel Sheet Column Title
    C#Tcp多个客户端与服务器数据与文件传输
    唯一分解定理
    欧拉函数
  • 原文地址:https://www.cnblogs.com/encounter/p/2188722.html
Copyright © 2020-2023  润新知