================================================================================ 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属性”所指向的那个对象。对于JavaScript“内置对象的构造 器”来说,它指向内部的一个原型。 每一个函数,无论它是否用作构造器,都会有一个独一无二的原型对象。缺省时 JavaScript用它构造出一个“空的初始对象实例(不是null)”。然而如果你给函 数的这个prototype赋一个新的对象,那么构造过程将用这个新对象作为“模板”。 接下来,构造过程将调用MyObject()来完成初始化。——注意,这里只是“初始 化”。 为了清楚地解释这个过程,我用代码形式化地描述一下这个过程: //--------------------------------------------------------- // new()关键字的形式化代码 //--------------------------------------------------------- function new(aFunction) { // 如果有参数args var _this = aFunction.prototype.clone(); // 从prototype中复制一个对象 aFunction.call(_this); // 调用构造函数完成初始化, (如果有,)传入args 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, ' '); } // 声明testFoo() function testFoo(v) { document.writeln(v, ' '); } // 声明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中的行为是一致的: //--------------------------------------------------------- // 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中实现。 这主要影响到构造阶段的一些事件和行为。——无法把一些“对象构造过程中” 的代码写到父类的构造器中。因为无论子类构造多少次,这次对象的构造过程根 本不会激活父类构造器中的代码。 所以再一次请大家看清楚new()关键字的形式代码中的这一行: //--------------------------------------------------------- // new()关键字的形式化代码 //--------------------------------------------------------- function new(aFunction) { // 如果有参数args var _this = aFunction.prototype.clone(); // 从prototype中复制一个对象 // ... } 这个过程中,JavaScript做的是“prototype.clone()”,而Delphi等其它语言做 的是“Inherited Create()”。 (本节未完待续...) |
|小黑屋|最新主题|手机版|微赢网络技术论坛 ( 苏ICP备08020429号 )
GMT+8, 2024-9-30 21:33 , Processed in 0.123064 second(s), 12 queries , Gzip On, MemCache On.
Powered by Discuz! X3.5
© 2001-2023 Discuz! Team.