>>>>>http://www.ibm.com/developerworks/cn/<<<<<

  JavaScript 早在发明初期时,仅用来进行简单的表单数据验证,但随着 Web 2.0 技术的蓬勃发展,尤其是近年来 Ajax 的异军突起,JavaScript 如今已成为 Internet 上最流行的脚本语言,用其开发的 web 应用也因高互动性极大的丰富了用户体验。而与此同时,当前的富英特网应用程序的代码量也产生了爆炸式的增长,因此利用面向对象的 JavaScript 编程,构建具有高复用性、易维护、易扩展性、健壮的浏览器端应用,具有重要意义。

  与高级语言 Java、C++ 等不同,JavaScript 本身并不是面向对象的语言,没有明确的类的概念,但可以通过核心语言来构建对象系统,模拟类及类的继承。但显然,构建类定义系统并不简单,庆幸的是,dojo.declare 可以帮助我们完成这项复杂的工作。本文将详细讲解如何利用 dojo.declare 定义类,实现单继承及多继承,及其他面向对象特性。为了更深入的理解 dojo.declare 模拟继承的原理,首先介绍原生 JavaScript 两种基本的继承方式,对象冒充及基于原型的继承。

JavaScript 继承方式

  关于继承可以朴素的理解为,通过继承,子类可以复用父类的方法,以达到代码重用。JavaScript 可用多种方式模拟继承,本文先举例介绍各种方法,后对比分析优缺点。

对象冒充

  在 JavaScript 中,构造函数也和普通的函数一样,可以被赋值和调用,对象冒充通过此原理来模拟继承。

清单 1. 对象冒充示例

             Function ClassA(name) { this.name = name; this.sayHello = function(){ alert("Hello, " +  this.name); } } Function ClassB(name,time) { this.newMethod = ClassA; this.newMethod(name); delete this.newMethod; this.time = time; this.sayGoodbye = function(){ alert("Goodbye " + this.name + ",it's " + this.time + " now !"); } } var objA = new ClassA("Tom"); var objB = new ClassB("Jerry","11:30am"); objA.sayHello(); // output is : "Hello,Tom"objB.sayHello(); // output is : "Hello,Jerry"objB.sayGoodbye();// output is : "Goodbye Jerry, it ’ s 11:30am now!"

如清单 1 所示,将 ClassA 的构造函数赋值为 ClassB 的一个普通方法,然后调用它,由于此时 this 指向的是 ClassB 的实例,那么 ClassB 的实例就会收到 ClassA 构造函数中定义的属性和方法,从而达到了继承的效果。

需要注意的是,应及时删除临时引用(this.newMethod),以防止 ClassB 更改 ClassA 类对象的引用。因为对临时引用(this.newMethod)的更改,也会导致 ClassA 的结构变化。并且 ClassB 的所有新属性和新方法,应该在删除临时引用后定义,否则,可能会覆盖父类的相关属性和方法。

认识到对象冒充的本质后,可以采用 JavaScript 中的 call 或者 apply 函数达到同样的效果,如清单 2 代码所示。其原理也是在调用 ClassA 的构造函数时,将 this 指向 ClassB 的实例。

清单 2. 利用 call() 实现对象冒充

              Function ClassB(name,time) { ClassA.call(this,name); // 或者 ClassA.apply(this,[name]); this.time = time; this.sayGoodbye = function(){ alert("Goodbye " + this.name + ",it's " + this.time + " now !"); } }

基于原型的继承

JavaScript 中的每个对象都包含一个原型对象(prototype),指向对某个对象的引用,而由于原型对象本身也是对象,则也会包含对它的原型的引用,由此构成一条原型链。原型链终止于内建 Object 类的原型。当要读取某个对象的属性或方法时,JavaScript 首先在该对象中查找,若没有找到,便在该对象的原型中继续查找,若仍未找到,便顺着原型链继续在原型的原型中查找,直到查找到或到达原型链的尽头。这样的系统被称为原型继承。而基于原型的继承,则是指利用了 prototype 或者说以某种方式覆盖了 prototype,从而达到属性及方法复用的目的。如下所示:

清单 3. 原型继承示例

               Function ClassA() { this.name = ""; this.sayHello = function(){ alert("Hello, " +  this.name); } } Function ClassB(){}; ClassB.prototype = new ClassA(); var objB = new ClassB(); objB.name = "Jerry"; objB.sayHello(); //output: "Hello,Jerry";

示例代码中将 ClassB 的 prototype 指向 ClassA 的实例,其原型链如图 1 所示,这样 ClassB 便拥有了 ClassA 所有的属性和方法,可以自由的赋值和调用。

图 1. ClassB 原型链

对比

对象冒充和基于原型的方式都可以让子类复用父类的代码以模拟继承,但这两种方法均各有利弊。

利用对象冒充,可以方便的实现多继承,只需要对所有需要继承的父类重复进行赋值流程便可。但这样却有着有明显的性能缺陷,因为在利用对象冒充模拟继承时,每个实例都会拥有一份父类成员变量和方法的副本,而成员方法只是一段对变量操作的可执行文本区域而已,这段区域不用为每个实例复制一份,所有的实例均可共享,这就造成了对内存资源的极度浪费。并且对象冒充也无法继承 prototype 域的变量和方法。

而基于原型的继承则可以使子类拥有一条完整的原型链,并且所有子类实例的原型都指向同一引用,相对于对象冒充,可极大的节省内存开销。但基于原型继承的缺陷也相当明显,就是父类的构造函数不能有参数,因为对子类 prototype 域的修改需在声明子类对象之后才能进行,而用子类构造函数的参数去初始化父类构造函数的属性是无法实现的。

因此可以结合二者的优点,采用混合的方式模拟继承,即用对象冒充的方式给属性赋值,用原型链的方式继承方法,示例代码如下:

清单 4. 混合方式继承示例

             Function ClassA(name){ this.name = name; } ClassA.prototype.sayHello = function(){ alert("Hello, " +  this.name); } Function ClassB(name,time){ ClassA.call(this, name); this.time = time; } ClassB.prototype = new ClassA(); ClassB.prototype.sayGoodbye = function(){ alert("Goodbye " + this.name + ",it's " + this.time + " now !"); } var objA = new  ClassA("Tom"); var objB = new  ClassB("Jerry","11:30am"); objA. sayHello();// output is: "Hello, Tom"objB.sayHello(); // output is: "Hello, Jerry"objB.sayGoodbye();//output is: "Goodbye Jerry,it ’ s 11:30am now !"

dojo.declare 详解

由上章的介绍可以发现,使用原始的 JavaScript 定义类并实现继承并不是件简单的工作,幸运的是 dojo.declare 提供了一整套的类定义机制,使我们可以像在高级语言中定义类一样,简单明确的完成类的声明。接下来将详细讲解 dojo.delcare 的用法及原理。

运用 dojo.declare 定义类

dojo.declare 方法的 API 如下,它有如下三个参数:

清单 5. dojo.declare 参数定义

              dojo.declare(/*String*/  className, /*Function | Function[]*/ superclass, /*Object*/ props )

className: 是要申明的类的类名,也就是创建的构造函数的名称。

superclass:所要继承的父类,此参数可为 null,表示没有父类,或者为一个父类,或为多个父类的数组,会在后续小节内详述。

props:散列体,由名、值(key, value)对组成,这个散列体将被添加到定义的类的原型对象中,也即为定义的类被其所有实例共享的属性及方法。其中,key :constructor 为保留字,此函数用来初始化新对象。

如清单 6 所示,我们用 dojo.declare 定义了一个名为 ClassX 的类,父类为空,即无父类,类的每个实例都有属性 messageX,在调用 constructor 函数初始化新对象时赋值,并为类的原型设置了 sayMessageX 方法。

清单 6. dojo.declare 定义类示例

                dojo.declare(
"ClassX", // 类的名字null,   // 父类// 要加入新定义类的原型的所有属性及方法{ messageX: null, constructor: function(msgX){ this.messageX = msgX; }, sayMessageX: function(){ alert("hi, this is " + this. messageX); } } );

定义类 ClassX 后,便可以使用了,由示例代码可见,类 ClassX 的对象既是 ClassX 的实例也是 Object 的实例。

清单 7. ClassX 使用示例

              var objX = new ClassX("X"); objX. sayMessageX(); //output: "hi, this is X"objX instanceof ClassX; //true objX instanceof Object;//true

此处以 ClassX 为例,介绍由 dojo.declare 申明的类的原型对象空间,如图 2 所示,主要包括以下属性:

  • declaredCalss: 函数名,即传给 dojo.declare 的第一个参数。
  • _constructor:类的初始化函数,即 props 中的 constructor 函数,当创建一个新对象时,由 dojo.declare 生成的构造函数自动调用此函数初始化所有的实例属性。
  • constructor:由 dojo.declare 为类生成的构造函数。
  • inherited:运用此方法可以调用父类的同名函数。
  • props 中定义的所有属性及方法,此处包括 messageX 和 sayMessageX。
  • 由于 ClassX 没有父类,则其原型的原型指向根原型。

图 2. ClassX 原型对象空间

单继承

假设有一个类 ClassZ,希望继承 ClassX,则可以用 dojo.declare 定义如下:

清单 8. 利用单继承定义 ClassZ 示例

              dojo.declare(
"ClassZ", // 类的名字ClassX,   // 父类// 要加入新定义类的原型的所有属性及方法{ messageZ: null, constructor: function(msgX,msgZ){ this.messageZ = msgZ; }, sayMessageZ: function(){ alert("hi, this is " + this.messageZ); } } ) var objZ = new ClassZ("X","Z"); objZ.sayMessageX();//output is: "hi, this is X"ojbZ.sayMessageZ();//output is: "hi, this is Z"

如代码所示,为了继承父类,仅需将父类作为 dojo.declare 的第二个参数就可以了。但其实 dojo.declare 帮我们做了以下两个工作:

  1. 构造新类的原型对象,并将其原型指向父类的原型。
  2. dojo.delcalre 为 ClassZ 生成的构造函数,将会先调用父类的构造函数,再调用子类的构造函数,这点与高级语言在继承中调用构造函数的顺序一致。如创建 ClassZ 对象,需执行类似以下的代码,这非常类似于上一章节中提到的混合继承方式。

清单 9. 创建 ClassZ 实际执行代码 ( 单继承 )

               Var objZ = new ClassZ("X","Z"); ClassX.apply(this,["X","Z"]); ClassZ._constructor.apply(this, ["X","Z"]);

ClassZ 的原型对象空间如下图所示,可以看到 ClassZ 类的原型引用了 ClassX 类的原型对象,则当如清单 8 中调用 ClassX 的方法时,首先将在 ClassZ 类的原型空间查找,在未查找到后,继而在 ClassX 的原型查找,查找到后调用。

图 3. ClassZ 原型对象空间 ( 单继承 )

多继承

假设有 ClassY 定义如下:

清单 10. ClassY 示例

                dojo.declare(
"ClassY", // 类的名字null,   // 父类{ messageY: null, constructor: function(msgY){ this.messageY = msgY; }, setMessageY: function(msgY){ this.messageY = msgY; }, sayMessageY: function(){ alert("hi, this is " + this.messageY); } } );

而 ClassZ 希望同时继承自 ClassX 及 ClassY,则可由 dojo.declare 定义如下:

清单 11. 利用多继承定义 ClassZ 示例

              dojo.declare(
"ClassZ", // 类的名字[ClassX,ClassY]   // 父类// 要加入新定义类的原型的所有属性及方法{ messageZ: null, constructor: function(msgX,msgY,msgZ){ this.messageZ = msgZ; this.setMessageY(msgY); }, sayMessageZ: function(){ alert("hi, this is " + this.messageZ); } } );

虽然我们仅将需继承的父类放入数组传递给 dojo.declare, 但这样定义出的类 ClassZ 却拥有 ClassX 与 ClassY 的全部功能。此处关于 ClassZ 初始化函数的详细讲解见下节。需注意的是,当将数组作为 superclass 参数时,仅第一个元素为新类的父类,之后的类会被当做为聚合类。聚合类中的属性和方法会被拷贝到新类中,以使新类具有聚合类的功能,从而达到模拟继承的效果。对于这种拷贝,除非属性保存的是数字,字符串,布尔,或者 null,否则均为引用拷贝。

下图为 ClassZ 的原型对象空间,由图可见,ClassZ 原型链的上一级指向的是 ClassX 的原型,则说明 ClassX 为其父类,而其原型中包含的 setMessageY 和 sayMessageY 函数则指向与 ClassY 原型对象中相同函数对象。

图 4. ClassZ 原型对象空间 ( 多继承 )

由于 dojo.declare 是用聚合类来模拟多继承,则 ClassZ 的实例是 ClassZ ,ClassX 类以及 Object 类的实例,但不会是 ClassY 类的实例,如下代码所示:

清单 12. ClassZ 使用示例

             var objZ = new  ClassZ("X","Y","Z"); objZ.sayMessageZ();//output is : "hi, this is Z"var check; check = objZ instanceof ClassZ; // true check = objZ instanceof ClassX; // true check = objZ instanceof Object; // true check = objZ instanceof ClassY; // false

预处理构造函数参数

对于继承和聚合,若子类,父类或聚合类之间的构造函数签名不同,则需要预处理构造函数的参数。

例如,ClassX(清单 6),ClassY(清单 10)及 ClassZ(清单 11) 的构造函数签名分别为:(msgX), (msgY), (msgX,msgY,msgZ) ; 当我们创建 ClassZ 的实例时,在默认情况下,dojo.declare 会按序先将参数传递给父类的构造函数,然后传递给聚合类的构造函数,最后传递给子类的初始化函数,如以下代码所示:

清单 13. 创建 ClassZ 实际执行代码 ( 多继承 )

               var objZ = new  ClassZ("X","Y","Z"); ClassX.apply(this,["X","Y","Z"]); ClassY.apply(this, ["X","Y","Z"]); ClassZ.prototype._constructor.apply(this, ["X","Y","Z"]);

由于 ClassX 仅需一个参数,因此可以忽略掉后两个参数“Y”,“Z”,被正确的初始化。而 ClassY 虽然也仅需一个参数,但传递给它的第一个参数却是“X”,因此它会将“X”当做“Y”,而被错误的初始化。为纠正这个错误,我们在 ClassZ 的构造函数中显示的调用了 setMessageY (msgY) 这个方法。 ClassZ 的初始化函数也可正确的接收这三个参数,它首先正确的初始化了 msgZ,后初始化 ClassY 类的实例变量。但由于父类及聚合类的构造函数会先于子类的初始化函数调用,则在 ClassZ 的初始化函数调用 setMessageY(msgY) 之前,属性 msgY 都被初始化错误。

大多数情况下,都可运用这种方法修复错误的初始化,但不正确的初始化有可能会带来一些坏的副作用,如抛出异常。

将散列表作为构造函数的参数则是一种更简单安全的做法。如,通过以下方式创建 ClassZ 的实例(清单 13 中第一行),需将本文中 ClassX,ClassY,ClassZ 的初始化函数重写如下:

清单 14. 初始化函数示例

               var ClassZ = new ClassZ({msgX:"X",msgY:"Y",msgZ:"Z"}); // ClassX constructor: function(args){ if(args && args.msgX) this.msgX = args.msgX; } // ClassY constructor: function(args){ if(args && args.msgY) this.msgY = args.msgY; } // ClassZ constructor: function(args){ if(args && args.msgZ) this.msgZ = args.msgZ; }

则通过此方式创建 ClassZ 的实例的过程中,不会有任何的初始化错误出现,并且此种方式也消除了对参数顺序的依赖。当参数个数过多时,将会是个不错的选择。


结束语

本文详细讲解了 dojo.declare 的各种用法,并通过实例及图解深入分析了其通过原型链及聚合类模拟面向对象的继承的原理。dojo.declare 不仅给 JavaScript 开发人员提供了一种自然的方式简洁明了的创建类,而且可通过已定义的类来组合新类,使人们可以更简单高效的复用代码。相信通过 dojo.declare,可以使您开发出更易扩展、易维护、更健壮的 web 应用程序!

参考资料

学习

  • 参考 Mastering Dojo(Craig Riecke, Rawld Gill, Alex Russell)。
  • 查看 Dojo.declare Reference Guide,了解更多 dojo.declare 技术细节。
  • 通过 JavaScript: The Definitive Guide,了解 JavaScript 基于原型链继承的详细内容。
  • 更多 dojo 技术,参考 developerWorks Dojo 技术专题。
  • 查看 HTML5 专题,了解更多和 HTML5 相关的知识和动向。
  • developerWorks Web development 专区:通过专门关于 Web 技术的文章和教程,扩展您在网站开发方面的技能。
  • developerWorks Ajax 资源中心:这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。
  • developerWorks Web 2.0 资源中心,这是有关 Web 2.0 相关信息的一站式中心,包括大量 Web 2.0 技术文章、教程、下载和相关技术资源。您还可以通过 Web 2.0 新手入门 栏目,迅速了解 Web 2.0 的相关概念。

转载于:https://www.cnblogs.com/happyPawpaw/archive/2012/05/31/2528928.html

(转)面向对象的 JavaScript 编程:dojo.declare 详解相关推荐

  1. 面向对象的 JavaScript 编程及其 Scope 处理

    为什么80%的码农都做不了架构师?>>>    在面向对象的 JavaScript 编程中,我们常常会将一些数据结构和操作封装成对象以达到继承和重用的目的.然而层层封装和继承再加上 ...

  2. 面向对象的JavaScript编程

    面向对象的JavaScript编程     Javascript对于做过Web程序的人不应该是陌生,初期是用来做一些简单的FORM验证,基本上是在玩弄一些技巧性的东西.IE 4.0引入了DHTML,同 ...

  3. python面向对象编程的三大特性_Python面向对象总结及类与正则表达式详解

    Python3 面向对象 -------------------------------------------------------------------------------- 一丶面向对象 ...

  4. C# 面向对象编程【多态详解】

    C# 面向对象编程[多态详解] 文章目录 C# 面向对象编程[多态详解] 1. 里氏转换 2. 多态 2.1 虚方法 3.2 抽象类 3.3 接口 1. 里氏转换 1).子类可以赋值给父类 2).如果 ...

  5. 众望所归的《JAVASCRIPT凌厉开发--EXT详解与实践 》终于上市了!

    大家好,我是这本书的策划编辑,经过努力,这本书终于上市了. 从创作开始,我们的目标就是写一本真正适合开发者参考和借鉴的EXT书,写作过程中,经过了无数次讨论和痛苦修订,感谢三位作者付出的艰辛劳动. 这 ...

  6. Javascript常用的设计模式详解

    Javascript常用的设计模式详解 阅读目录 一:理解工厂模式 二:理解单体模式 三:理解模块模式 四:理解代理模式 五:理解职责链模式 六:命令模式的理解: 七:模板方法模式 八:理解javas ...

  7. Java编程配置思路详解

    Java编程配置思路详解 SpringBoot虽然提供了很多优秀的starter帮助我们快速开发,可实际生产环境的特殊性,我们依然需要对默认整合配置做自定义操作,提高程序的可控性,虽然你配的不一定比官 ...

  8. JavaScript中getBoundingClientRect()方法详解

    JavaScript中getBoundingClientRect()方法详解 getBoundingClientRect() 这个方法返回一个矩形对象,包含四个属性:left.top.right和bo ...

  9. oracle分区表编程,Oracle分区表详解

    当前位置:我的异常网» 编程 » Oracle分区表详解 Oracle分区表详解 www.myexceptions.net  网友分享于:2013-10-28  浏览:25次 Oracle分区表详解 ...

最新文章

  1. SCCM 2007系列7 补丁分发上
  2. 化浆池是什么东西_头次见聪明人在阳台上砌洗衣池,开始被人笑话,装完都跟着学...
  3. 快速排序 python菜鸟教程-快速排序
  4. “人工智能的数理基础”主题论坛,五位学者从数学角度解决AI问题
  5. 大学物理实验试卷1到8_物理实验在绝对的“理论”面前就是“纸老虎”
  6. [Abp vNext微服务实践] - 搭建租户管理服务
  7. 推荐 -- 极客导航,让工作学习更有效率
  8. MySQL故障检测_mysql主从故障检测处理脚本
  9. logfile switch causes incremental checkpoint?
  10. iframe页面找父页面的元素
  11. 中兴手机官宣吴京代言 以科技为勇敢者助力
  12. HappyAA服务器部署笔记1(nginx+tomcat的安装与配置)
  13. tomcat lb cluster
  14. 设置电脑分屏显示的4种情况
  15. 【HAVENT原创】Eureka 注册中心服务上线下线邮件提醒
  16. FRP内网穿透搭建-无公网IP时外部访问服务解决办法
  17. 自学java后都是怎么找的工作?
  18. CSS(二)——Flex布局 边框 渐变 过渡 动画
  19. JDK 17:Java 17 中的新特性
  20. 【JDM】弯道王子,最强马6,马自达Mazda 6 MPS

热门文章

  1. 浙江大学计算机研究生分数线初试单科学科,2016年浙江大学计算机考研复试分数线_浙江大学考研分数线...
  2. r语言siggenes包_初探R语言可视化交互式包plotly——旭日图(Sunburst Chart)
  3. java将0到9随机输出_生成0到9之间的随机整数
  4. tdk怎么设置_不知道怎么分析对手网站?看这里!
  5. springboot 集成jpa_基于Spring Boot+JPA Restful 风格的数据
  6. 中山学院计算机学院家长座谈会,计算机科学与技术学院举办校友座谈会
  7. linux命令的使用实验报告,Linux实验报告一-常用命令使用.doc
  8. freescale imx6 编译 linux ltib,TQIMX6Q技术分享——LTIB安装配置(转)
  9. 【MySQL】MySQL的索引
  10. 不使用梯度裁剪和使用梯度裁剪的对比(tensorflow)