zoukankan      html  css  js  c++  java
  • 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()”。

    (本节未完待续...)

  • 相关阅读:
    centos8 将SSSD配置为使用LDAP并要求TLS身份验证
    Centos8 搭建 kafka2.8 .net5 简单使用kafka
    .net core 3.1 ActionFilter 拦截器 偶然 OnActionExecuting 中HttpContext.Session.Id 为空字符串 的问题
    Springboot根据不同环境加载对应的配置
    VMware Workstation12 安装 Centos8.3
    .net core json配置文件小结
    springboot mybatisplus createtime和updatetime自动填充
    .net core autofac依赖注入简洁版
    .Net Core 使用 redis 存储 session
    .Net Core 接入 RocketMQ
  • 原文地址:https://www.cnblogs.com/encounter/p/2188722.html
Copyright © 2011-2022 走看看