一直想对Javascript再次做一些总结,正好最近自己写了一个小型Js UI库,总结了一下Js的继承机制,在网上也看了一些前辈们博客里的总结,感觉分析不是特别全面。这里仅仅是把自己的学习体会拿出来分享一下,希望对大家学习Javascript有所帮助。
Javascript本身是从Perl语言的语法演变而来的,本质上是脚本语言,随着版本的更新逐渐加入的对面向对象的模拟。我认为Js的面向对象模拟总体上做得还是不错的,因为我们不能盲从任何一种理念,不能纯粹的为了OOP而OOP,我们需要抓住的是面向对象的好处到底是什么?为了这些优点去OOP,才是最明智的选择,所以说Js做得还不错。
Js的继承在很多书里面细致的分了很多种类型和实现方式,大体上就是两种:对象冒充、原型方式。这两种方式各有优点和缺陷,这里我先列举出来,再从底层分析区别:
(一)对象冒充
- function A(name){
- this.name = name;
- this.sayHello = function(){alert(this.name+” say Hello!”);};
- }
- function B(name,id){
- this.temp = A;
- this.temp(name); //相当于new A();
- delete this.temp; //防止在以后通过temp引用覆盖超类A的属性和方法
- this.id = id;
- this.checkId = function(ID){alert(this.id==ID)};
- }
当构造对象B的时候,调用temp相当于启动A的构造函数,注意这里的上下文环境中的this对象是B的实例,所以在执行A构造函数脚本时,所有A的变量和方法都会赋值给this所指的对象,即B的实例,这样子就达到B继承了A的属性方法的目的。之后删除临时引用temp,是防止维护B中对A的类对象(注意不是实例对象)的引用更改,因为更改temp会直接导致类A(注意不是类A的对象)结构的变化。
我们看到了,在Js版本更新的过程中,为了更方便的执行这种上下文this的切换以达到继承或者更加广义的目的,增加了call和apply函数。它们的原理是一样的,只是参数不同的版本罢了(一个可变任意参数,一个必须传入数组作为参数集合)。这里就以call为例子,解释一下用call实现的对象冒充继承。
- function Rect(width, height){
- this.width = width;
- this.height = height;
- this.area = function(){return this.width*this.height;};
- }
- function myRect(width, height, name){
- Rect .call(this,width,height);
- this.name = name;
- this.show = function(){
- alert(this.name+” with area:”+this.area());
- }
- }
关于Call方法,官方解释:调用一个对象的一个方法,以另一个对象替换当前对象。
call (thisOb,arg1, arg2…)
这也是一种对象冒充的继承,其实在call方法调用的时候发生的事情也是上下文环境变量this的替换,在myRect函数体中this肯定是指向类myRect对象的实例了,然而用这个this作为上下文环境变量调用名字叫Rect方法,即类Rect的构造函数。于是此时调用Rect时候对this的赋值属性和方法都实际上是对一个myRect的对象进行。所以说尽管call和apply并不是仅仅为了继承而新增的方法,但用它们可以模拟继承。
对象冒充继承就是这么一回事,它可以实现多重继承,只要重复做这一套赋值的流程就可以了。不过目前真正大规模使用得并不多,为什么呢?因为它有一个明显的性能缺陷,这就要说道OO的概念了,我们说对象是成员+成员方法的集合,构造对象实例的时候,这些实例只需要拥有各自的成员变量就可以了,成员方法只是一段对变量操作的可执行文本区域而已,这段区域不用为每个实例而复制一份,所有的实例都可以共享。现在回到Js利用对象冒充模拟的继承里,所有的成员方法都是针对this而创建的,也就是所所有的实例都会拥有一份成员方法的副本,这是对内存资源的一种极度浪费。其它的缺陷比如说对象冒充无法继承prototype域的变量和方法就不用提了,笔者认为前一个致命缺陷就已经足够。不过,我们还是需要理解它,特别是父类的属性和方法是如何继承下来的原理,对于理解Js继承很重要。
(二)原型方式
第二种继承方式是原型方式,所谓原型方式的继承,是指利用了prototype或者说以某种方式覆盖了prototype,从而达到属性方法复制的目的。其实现方式有很多中,可能不同框架多少会有一点区别,但是我们把握住原理,就不会有任何不理解的地方了。看一个例子(某一种实现):
- function Person(){
- this.name = “Mike”;
- this.sayGoodbye = function(){alert(“GoodBye!”);};
- }
- Person.prototype.sayHello = function(){alert(”Hello!”);};
- function Student(){}
- Student.prototype = new Person();
关键是对最后一句Student原型属性赋值为Person类构造的对象,这里笔者解释一下父类的属性和方法是如何copy到子类上的。Js对象在读取某个对象属性的时候,总是先查看自身域的属性列表,如果有就返回否则去读取prototype域(每个对象共享构造对象的类的prototype域所有属性和方法),如果找到就返回,由于prototype可以指向别的对象,所以Js解释器会递归的去查找prototype域指向对象的prototype域,直到prototype为本身,查找变成了一种循环,就停止,此时还没找到就成undefined了。
这样看来,最后一句发生的效果就是将父类所有属性和方法连接到子类的prototype域上,这样子类就继承了父类所有的属性和方法,包括name、sayGoodbye和sayHello。这里与其把最后一句看成一种赋值,不如理解成一种指向关系更好一点。这种原型继承的缺陷也相当明显,就是继承时父类的构造函数时不能带参数,因为对子类prototype域的修改是在声明子类对象之后才能进行,用子类构造函数的参数去初始化父类属性是无法实现的,如下所示:
- function Person(name){
- this.name = name;
- }
- function Student(name,id){
- this.id = id;
- }
- Student.prototype = new Person(this.name);
两种继承方式已经讲完了,如果我们理解了两种方式下子类如何把父类的属性和方法“抓取”下来,就可以自由组合各自的利弊,来实现真正合理的Js继承。下面是个人总结的一种综合方式:
- function Person(name){
- this.name = name;
- }
- Person.prototype.sayHello = function(){alert(this.name+“say Hello!”);};
- function Student(name,id){
- Person.call(this,name);
- this.id = id;
- }
- Student.prototype = new Person();
- Student.prototype.show = function(){
- alert(“Name is:”+ this.name+” and Id is:”+this.id);
- }
总结就是利用对象冒充机制的call方法把父类的属性给抓取下来,而成员方法尽量写进被所有对象实例共享的prototype域中,以防止方法副本重复创建。然后子类继承父类prototype域来抓取下来所有的方法。如想彻底理清这些调用链的关系,推荐大家多关注Js中prototype的constructor和对象的constructor属性,这里就不多说了。
分享到:
相关推荐
继承有两种方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。 由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且实现继承主要依靠原型链来实现。 ...
JavaScript的继承在很多书里面细致的分了很多种类型和实现方式,大体上就是两种:对象冒充、原型方式。这两种方式各有优点和缺陷,这里我先列举出来,再从底层分析区别
这篇文章主要介绍了JavaScript原型继承和原型链原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在讨论原型继承之前,先回顾一下关于创建自定义类型的...
首先,在js中,给对象定义属性有两种方式: //通过执行构造函数设置属性 function A(){ this.a = 1; } //通过原型设置属性 A.prototype.b = 1; 所以: 一个类Sub要继承另一个类Super,需要继承父类的prototype下的...
实际上,几何形状只有两种,即椭圆形(是圆形的)和多边形(具有一定数量的边)。圆是椭圆的一种,它只有一个焦点。三角形、矩形和五边形都是多边形的一种,具有不同数量的边。正方形是矩形的一种,所有的边等长。这就...
首先来分析构造函数和原型链两种实现继承方式的缺陷: 构造函数(对象冒充)的主要问题是必须使用构造函数方式,且无法继承通过原型定义的方法,这不是最好的选择。不过如果使用原型链,就无法使用带参数的构造函数...
JavaScript中对象创建的方式有两种:工厂方法(Factory Functions)、构造器方法(Constructor Functions) 。 工厂方法 工厂方法在编程领域是一个非类或构造器的返回对象的方法。在JavaScript中,任何返回不使
在之前的两篇博客中,我们详细探讨了JavaScript OOP中的各种知识点(JS OOP基础与JS 中This指向详解 、 成员属性、静态属性、原型属性与JS原型链)。今天我们来继续探讨剩余的内容吧。 我们都知道,面向对象的三大...
本文将介绍两种很类似于对象冒充的继承方式,即使用call()和apply()方法
Object是在javascript中一个被我们经常使用的类型,而且JS中的所有对象都是继承自Object对象的。...那在Javascript中,创建对象的方式通常有两种方式:构造函数和对象字面量。 new构造函数法 var person = new O
新对象创建的两种方法 var newObject={}; var newObject=new object();//object 构造器的简洁记法 2、基本Constructor Javascript不支持类的情况下对象与Constructor,通过new关键字实例化一个对象,代码大概是...
一般的面向对象程序语言,有两种继承方法——接口继承(interface inheritance)和实现继承(implementation inheritance)。接口继承只继承方法签名,而实现继承则继承实际的方法。在JavaScript中,函数没有签名,...
这里接口的意思是Observable实际上起了一个抽象类的作用,Extjs中有大量的组件都是继承自这个类的。...另外,博客园内还有一个写的很好的系列 JavaScript继承详解. 他主要是根据Douglas Crockford的两篇文章写的
在JavaScript的大世界里讨论面向对象,都要提到两点:1.JavaScript是一门基于原型的面向对象语言 2.模拟类语言的面向对象方式。对于为什么要模拟类语言的面向对象,我个人认为:某些情况下,原型模式能够提供一定...
本文实例讲述了ES6新特性之模块...在ES6之前,社区制定了一些模块加载方案,最主要的有CommonJS和AMD两种。前者用于服务器,后者用于浏览器。ES6在语言规格的层面上,实现了模块功能,而且实现得相当简单,完全可以取代
类的加入虽然有助于其它语言的使用者开始使用JavaScript,但是却无法发挥出JavaScript原型继承的巨大优势);以及为了保持非侵入式弥补其它新特性而诞生的Symbols。 其它的新特性也相当诱人,熟练掌握可以大幅度提升...
3.3 render呈现控件的几种方式 30 3.3.1 使用htmltextwriter类输出 30 3.3.2 直接输出html标签 32 3.3.3 使用服务器控件的rendercontrol方法 33 3.4 addattributestorender方法 34 3.5 createchildcontrols方法...
3.3 render呈现控件的几种方式 30 3.3.1 使用htmltextwriter类输出 30 3.3.2 直接输出html标签 32 3.3.3 使用服务器控件的rendercontrol方法 33 3.4 addattributestorender方法 34 3.5 createchildcontrols方法...
3.3 render呈现控件的几种方式 30 3.3.1 使用htmltextwriter类输出 30 3.3.2 直接输出html标签 32 3.3.3 使用服务器控件的rendercontrol方法 33 3.4 addattributestorender方法 34 3.5 createchildcontrols方法...
3.3 render呈现控件的几种方式30 3.3.1 使用htmltextwriter类输出30 3.3.2 直接输出html标签32 3.3.3 使用服务器控件的rendercontrol方法33 3.4 addattributestorender方法34 3.5 createchildcontrols方法35 ...