声明和约定:

在C++和Java中,我们可以通过关键字class来声明一个类,在JavaScript中没有这个关键字,但我们知道可以通过new一个function创建对象,这个function类似C++和Java中的class,这个function叫做类或者类的构造函数,或者说通过new创建对象的函数叫做构造函数,作为父类时,叫做父类构造函数,作为子类时,叫做子类构造函数;通过构造函数new出来的对象,作为子类时,叫做子类对象或者子类的实例,作为父类时,叫做父类对象或者父类的实例。

1. 原型链继承

继承是OO语言中非常重要的一个特性,在C++、Java中实现继承都非常简单,因为这类语言原生支持继承,只是使用符号或者关键字即可完成继承,但是在JavaScript中实现继承没有那么容易,只能依靠JavaScript语言本身的其它特性来实现,这个特性也就是原型链。设想如下:使用组合构造函数和原型模式的方式创建一个父类构造函数,然后以同样的方式创建一个子类构造函数,使用父类构造函数new一个对象并且将其赋值给子类构造函数的prototype属性,这样子类的构造函数的原型对象就是new出来的父类对象,而父类对象中拥有父类所有的属性和父类共享的方法,因此实现了继承,代码:

function SuperClass(){
this.property="super"
}
SuperClass.prototype.showProperty=function(){
console.log(this.property);
} function SubClass(){
this.name="sub"
} SubClass.prototype

=new SuperClass(); SubClass.prototype.showName=function(){ console.log(this

.name);
} var sub1=new SubClass();
sub1.showProperty(); //super
sub1.showName(); //sub

以上代码中需要注意的就是必须先给子类构造函数的prototype属性赋值为父类的实例,然后才可以向子类构造函数的prototype添加共有的方法,因为如果先给子类的构造函数添加方法然后再赋值为父类的实例,相当于子类构造函数的的prototype先指向了默认创建的原型对象,先添加方法就把方法添加到了默认的原型对象,此时再赋值为父类的实例,相当于切断了子类构造函数与默认创建的原型对象之间的关系,而将prototype指向了父类的实例,此时的父类的实例并没有添加的方法,所以在子类对象执行自己的函数时根本找不到对应的方法。

在以上代码的基础上添加测试代码后如下:

function SuperClass(){
this.property="super"
this.color=["red","blue"];
}
SuperClass.prototype.showProperty=function(){
console.log(this.property);
} function SubClass(){
this.name="sub"
} SubClass.prototype=new SuperClass();
SubClass.prototype.showName=function(){
console.log(this.name);
} var sub1=new SubClass();
sub1.showProperty(); //super
sub1.showName(); //sub var sub2=new SubClass();
sub2.showProperty(); //super
sub2.showName(); //sub sub1.property

="modified"


    sub1.showProperty();  

//modified sub2.showProperty(); //super

    sub1.name="sub1";
sub1.showName(); //sub1
sub2.showName(); //sub console.log(sub1.color.toString()); //red,blue
console.log(sub2.color.toString()); //red,blue sub1.color.push(

"yellow"); console.log(sub1.color.toString()); //red,blue,yellow console.log(sub2.color.toString()); //red,blue,yellow

代码中所有的输出结果都以注释给出了,其中代码标注的地方输出结果值得研究,第一块标注的地方修改了sub1的property属性,输出sub1和sub2的property属性,但结果sub1的property属性是modified,sub2输出结果是super。第二块标注的地方明明只修改了sub1的color属性,但是sub1和sub2的color属性输出结果一致。出现这种情况的原因是:我们将父类对象作为子类构造函数的原型对象,所以父类对象中所有的属性和方法在子类对象中均是共享的,也就是说,所有的子类对象共享一份父类对象的属性和方法,所以在第二块代码标注的地方出现了修改sub1的color属性,sub2的color属性也改变了,而且与sub1的color属性完全一致。至于第一块标注的地方,修改sub1的property属性,而sub2的属性没有发生变化,是因为给sub1的property属性赋值,相当于给sub1添加了一个自己的属性property并且赋值为modified,而并非是给sub1原型的property属性赋值,当调用sub1的showProperty函数的时候,优先从sub1的自己的属性里搜索属性名,所以输出结果是modified,而sub2并没有自己的property属性,所以输出的property属性是sub1和sub2共有的原型里的属性,所以输出结果是super。

这种方式实现继承还有个缺点就是无法向父类构造函数传参,有如下代码:

function Person(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex;
} Person.prototype.sayName=function(){
console.log(this.name);
} function Student(name,age,sex,grade){
this.grade=grade;
} Student.prototype

=new

 Person();
student.prototype.sayGrade=function(){
console.log(this.grade);
}

代码标注的地方是这种方式实现继承的关键,而且也是唯一用到父类构造函数的地方,如果在此时将参数传入父类的构造函数,那么所有的新创建的子类对象中父类的属性值将会完全一致,这样的继承失去了活性,根本没有意义。

总的来说,这种方式也就是仅仅实现了继承,优点几乎没有,缺点有:1.没有完成封装,子类实现继承时的代码均在全局作用域中。2.无法向父类的构造函数传参,即使传参了也没有意义。3.所有子类对象共享父类对象的属性和方法。其中第三点是最为致命的一点。

2. 借用构造函数

这种方式也叫伪造对象或者经典继承,思想就是在子类的构造函数中调用父类的构造函数,并将执行环境传入父类的构造函数。如下:

function Person(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex; if(typeof arguments.callee.sayName != "function"){
arguments.callee.prototype.sayName=function(){
console.log(this.name);
}
}
} function Student(name,age,sex,grade){
Person.call(this,name,age,sex);
this.grade=grade;
} var stu1=new Student("yangyule",23,"male",3); console.log(stu1.name);

这种方式完美的解决了父类的属性变成子类对象共有属性的问题,需要注意的就是在子类构造函数中调用父类构造函数的时候,需要将this作为参数传入父类构造函数的call函数中,这样在父类函数中的this即为新创建的子类对象,如果没有传入this,直接在子类构造函数中调用父类构造函数,父类函数中的this为全局对象,在浏览器中运行代码的时候即为window,无法实现继承。但是仅仅使用这种方式实现继承缺点也比较严重,就是父类的方法子类没有办法继承,不管父类是通过组合构造函数和原型模式还是动态原型模式,都无法避免这个问题,但可以使用组合继承来解决。

3. 组合继承

组合继承就是组合原型链继承和借用构造函数继承的继承方式,这种继承也叫作伪经典继承,是使用最多的继承。这种继承在继承父类属性的时候使用借用构造函数的方式,继承父类方法的时候使用原型链继承,所以避免了单独使用这两种继承方式带来的问题。

function Person(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex; if(typeof this.sayName != "function"){
arguments.callee.prototype.sayName=function(){
console.log(this.name);
}
}
} var person1=new Person("yangyule",23,"male");
person1.sayName(); //yangyule function Student(name,age,sex,grade){
Person.call(this,name,age,sex);
this.grade=grade;
} Student.prototype=new Person();
Student.prototype.sayGrade=function(){
console.log(this.grade);
} var stu1=new Student("vail",23,"male",4);
stu1.sayName(); //vail
stu1.sayGrade(); // var stu2=new Student("hale",23,"male",3);
stu2.sayName(); //hale
stu2.sayGrade(); // stu1.age=21;
console.log(stu1.age); //
console.log(stu2.age); //

这种方式解决了主要问题,但带来了新的问题,虽然所有从父类继承而来的属性在子类对象中也是私有属性,不是共享属性(因为在子类的构造函数中调用了父类构造函数,并传入了this),但是由于把子类构造函数的原型对象设置成了父类对象,所以在子类对象的原型中会有一份来自父类的属性存在,也就是说每个子类对象中其实有两份一模一样的来自父类的属性,一份存在于子类对象中,另一份存在于子类对象的原型中,由于访问实例的属性的时候优先搜索自己的属性,所以修改了stu1的age,实际上是修改了stu1自己的age属性,而不是stu1原型中age属性的值,所以stu2的age属性还是23,而不是21。来自父类的属性,除了每个子类对象私有之外,内存中还存在一个子类对象的原型对象,这些来自父类的属性也存在于子类对象的原型对象中,所以造成了内存空间的少部分浪费。这种方式需要注意的就是使用原型链继承父类方法的时候,必须先将子类构造函数的prototype属性设置为父类对象,然后在向子类构造函数的prototype添加方法,和使用原型链实现继承是一样的。这种方式还有一个问题就是封装性,因为要实现继承父类的方法,所以不得不在全局环境下写更多的代码。

4. 完美继承

这种模式是我自创,解决了其它继承模式的一些问题。我学习编程语言是从其他经典OO语言开始的,所以我一直特别在意构造函数的封装性,希望把与类相关的属性和函数写在构造函数中,因此创建对象我特别愿意使用动态原型模式,而我的这种继承方式也要求构造函数必须使用动态原型模式创建。这种继承的思想是在父类的构造函数中检查函数的caller属性是否为空,如果不为空的话,使用for in遍历父类构造函数的原型对象(for in并不会遍历对象的原型中的方法和属性,函数的原型对象是对象,所以使用for in只会遍历函数的原型对象,而不会遍历原型对象的原型对象),将其所有的属性和方法赋值给caller.prototype,继承的时候在子类的构造函数中使用父类构造函数的call方法,并将this作为参数传入,即可完成继承。

function Person(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex; if(typeof arguments.callee.sayName != "function"){
arguments.callee.prototype.sayName=function(){
console.log(this.name);
}
} if(arguments.callee.caller != null && typeof arguments.callee.caller.sayName != "function"){
for(key in arguments.callee.prototype){
//console.log(key +": "+arguments.callee.prototype[key]);
arguments.callee.caller.prototype[key]=arguments.callee.prototype[key];
}
}
} var person1=new Person("yangyule",23,"male");
person1.sayName(); //yangyule function Student(name,age,sex,grade){
Person.call(this,name,age,sex);
this.grade=grade; if(typeof arguments.callee.sayGrade != "function"){
arguments.callee.prototype.sayGrade=function(){
console.log(this.grade);
}
} if(arguments.callee.caller != null && typeof arguments.callee.caller.sayGrade != "function"){
for(key in arguments.callee.prototype){
//console.log(key +": "+arguments.callee.prototype[key]);
arguments.callee.caller.prototype[key]=arguments.callee.prototype[key];
}
}
} var student1=new Student("vile",23,"male",3);
student1.sayName(); //vile
student1.sayGrade(); // function Undergraduate(name,age,sex,grade,major){
Student.call(this,name,age,sex,grade);
this.major=major;
} var cstu1=new Undergraduate("vhile",23,"male",4,"software");
cstu1.sayName(); //vhile
cstu1.sayGrade(); // var cstu2=new Undergraduate("vilne",23,"male",2,"math");
cstu2.sayName(); //vilne
cstu2.sayGrade(); // console.log(cstu1 instanceof Undergraduate); //true
console.log(cstu1 instanceof Student); //false
console.log(cstu1 instanceof Person); //false
console.log(cstu1.constructor); //function Undergraduate(...)...

以上代码中除了判断caller是否为空,还判断了caller中是否有继承而来的函数,这样做避免了每次创建对象的时候都为子类构造函数的prototype赋值,优化了执行性能。从上述的代码可以看出,Undergraduate类继承了Student类,Student类又继承了Person类,所以Undergraduate类对象既可以使用Student类中方法,也可以使用Person类中的方法,实现了连续继承。这种继承方式不仅没有破坏封装性,而且还可以检测对象类型、代码量更少,每次代码的变动也不大,每次只需要将检查函数的函数名改为该构造函数原型中的任意一个函数名即可。Java中没有C++中的多继承,但是Java有接口,而本方式实现了类似C++的多继承,只需要在子类的构造函数中调用所有父类的构造函数即可,这种方式是所有方式里唯一实现了多继承的方式。

function SuperClass1(name){
this.name=name;
if(typeof this.sayName != "function"){
arguments.callee.prototype.sayName=function(){
console.log(this.name);
}
} if(arguments.callee.caller != null && typeof arguments.callee.caller.sayName != "function"){
for(key in arguments.callee.prototype){
arguments.callee.caller.prototype[key]=arguments.callee.prototype[key];
}
}
} function SuperClass2(age){
this.age=age;
if(typeof this.sayAge != "function"){
arguments.callee.prototype.sayAge=function(){
console.log(this.age);
}
} if(arguments.callee.caller != null && typeof arguments.callee.caller.sayAge != "function"){
for(key in arguments.callee.prototype){
arguments.callee.caller.prototype[key]=arguments.callee.prototype[key];
}
}
} function SubClass(name,age,property){
SuperClass1.call(this,name);
SuperClass2.call(this,age);
this.property=property; if(typeof this.sayProperty != "function"){
arguments.callee.prototype.sayProperty=function(){
console.log(this.property);
}
}
} var sub1=new SubClass("yangyule",23,"I am an object");
sub1.sayName(); //yangyule
sub1.sayAge(); //
sub1.sayProperty(); //I am an object

以上代码中SubClass继承了SuperClass1和SuperClass2,并成功调用了相应的函数。

完美模式并非一个缺点都没有,因为用到了函数的callee和caller,在严格模式下运行会导致错误。但也仅仅只有这一个微不足道的缺点,大家可以放心的使用。完美模式是我最推荐使用的继承方式。

5. 原型式继承

这种继承方式虽然也是借助于原型链实现继承,但是和原型链继承完全不同,而且它的主要思想和原型的关系也不大(所有的继承都是依赖于原型),至于为什么叫原型式继承,我在《JavaScript高级程序设计》第三版中见到此方式这样叫,所以我也就这样叫了。

之前的几种方式,我们都是竭尽所能的让JavaScript模仿经典的OO语言实现继承,我们不妨换个方式思考一下问题,JavaScript语言相当灵活,我们与其让两个类实现继承,不如直接把父类对象当做一个空对象的原型,在需要的时候向空对象添加属性和方法不是更好吗?

function Person(name,friends){
this.name=name;
this.friends=friends; if(typeof this.sayFriends != "function"){
arguments.callee.prototype.sayFriends=function(){
console.log(this.friends.toString());
}
}
} var student1=Object.create(new Person("yangyule",["Bart","Devin"]));
student1.grade=3; student1.sayFriends(); //Bart,Devin student2=Object.create(new Person("vile",["YangYule","Evan"]));
student2.grade=4; student2.sayFriends(); //YangYule,Evan
student1.sayFriends(); //Bart,Devin

其中的Object.create函数接受一个必需的参数和一个可选的参数,第一个参数将作为新对象的构造函数的原型对象,第二参数为新对象额外定义的属性的对象。当给Object.create函数传入一个参数时,它的行为如下:

function create(o){
function F(){};
F.prototype=o;
return new F();
}

当给create函数传入的对象相同时,产生的对象就是继承自同一个对象。这种继承方式适合对象间的继承,而不是类间的继承,也就是说在不需要批量使用子类对象的时候适合使用这种方式。

6. 寄生式继承

寄生式继承的主要思想是将父类对象拷贝一份,然后再添加需要的方法,这和工厂模式的思想类似。

function createAnother(original){
var clone = Object(original);
clone.sayHi=function(){
alert("hi");
}
return clone;
}
var me = {name:"yangyule"};
var clone = createAnother(me);
clone.sayHi();

这种方式子类继承了父类所有的属性和方法,而且还可以根据需要添加方法,但是缺点是函数不能复用

7. 寄生组合式继承

这种继承方式就比较扯淡了,这种继承方式的提出主要是解决组合继承调用两次构造函数,造成子类对象和子类原型对象中存在两份父类属性的问题,它的主要思想是在子类的构造函数中调用一次父类构造函数,然后将子类构造函数的prototype属性设置为父类构造函数的原型对象的拷贝对象,这样就解决了组合继承带来的问题。但这样做远不如完美继承来的痛快~~~

function Person(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex; if(typeof this.sayName != "function"){
arguments.callee.prototype.sayName=function(){
console.log(this.name);
}
}
} function Student(name,age,sex,grade){
Person.call(this,name,age,sex);
this.grade=grade;
} Student.prototype=Object.create(Person.prototype);
Student.prototype.sayGrade=function(){
console.log(this.grade);
} var student1=new Student("yangyule",23,"male",3);
student1.sayName(); //yangyule
student1.sayGrade(); // console.log(Person.prototype.sayGrade); //undefined

JavaScript高级特性-实现继承的七种方式的更多相关文章

  1. 《JAVASCRIPT高级程序设计》创建对象的七种模式

    细看javascript创建对象模式的诞生,具体的脉络为:不使用任何模式——工厂模式——构造函数模式——原型模式——组合使用构造函数模式——动态原型模式——寄生构造函数模式——稳妥构造函数模式.每一种 ...

  2. javascript 模拟java 实现继承的5种方式

    1.继承第一种方式:对象冒充 function Parent(username){ this.username = username; this.hello = function(){ alert(t ...

  3. Javascript中用来实现继承的几种方式

    一.原型链继承 原理:修改子类型的原型,使其指向父类型的实例: 缺点: 1,不能以字面量方式在子类型的原型上添加新方法:这回重新改写子类型的原型: 2  创建子类型的实例时无法向父类型的构造函数传参. ...

  4. JavaScript高级特性-创建对象的九种方式

    1. 对象字面量 通过这种方式创建对象极为简单,将属性名用引号括起来,再将属性名和属性值之间以冒号分隔,各属性名值对之后用逗号隔开,最后一个属性不用逗号隔开,所有的属性名值对用大括号括起来,像这样: ...

  5. javascript高级特性

    01_javascript相关内容02_函数_Arguments对象03_函数_变量的作用域04_函数_特殊函数05_闭包_作用域链&闭包06_闭包_循环中的闭包07_对象_定义普通对象08_ ...

  6. javascript高级特性(面向对象)

    javascript高级特性(面向对象): * 面向对象: * 面向对象和面向过程的区别: * 面向对象:人就是对象,年龄\性别就是属性,出生\上学\结婚就是方法. * 面向过程:人出生.上学.工作. ...

  7. JavaScript高级特性-数组

    1. JavaScript中的数组 在C++.Java中,数组是一种高效的数据结构,随机访问性能特别好,但是局限性也特别明显,就是数组中存放的数据必须是同一类型的,而在JavaScript中,数组中的 ...

  8. Java第四次作业,面向对象高级特性(继承和多态)

    Java第四次作业-面向对象高级特性(继承和多态) (一)学习总结 1.学习使用思维导图对Java面向对象编程的知识点(封装.继承和多态)进行总结. 2.阅读下面程序,分析是否能编译通过?如果不能,说 ...

  9. Java第四次作业—面向对象高级特性(继承和多态)

    Java第四次作业-面向对象高级特性(继承和多态) (一)学习总结 1.学习使用思维导图对Java面向对象编程的知识点(封装.继承和多态)进行总结. 2.阅读下面程序,分析是否能编译通过?如果不能,说 ...

随机推荐

  1. linux装tomcat遇到的坑

    最开始通过apt-get安装,各种毛病 然后下载tar.gz压缩包解压使用,运行startup.sh可以启动,但是看日志发现 Tomcat启动时卡在 INFO HostConfig.deployDir ...

  2. GTest的安装与使用

    安装GTest 1.安装源代码 下载gtest,release-1.8.0 git clone https://github.com/google/googletest gtest编译 cd goog ...

  3. PostgreSQL 数据类型

    数值类型 数值类型由两个字节,4字节和8字节的整数,4字节和8字节的浮点数和可选精度的小数.下表列出了可用的类型. www.yiibai.com Name Storage Size Descripti ...

  4. Java关键字(一)——instanceof

    instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为: boolean result = obj instanceof Class 其中 obj 为一 ...

  5. 对于Ext.data.Store 介紹 与总结,以及对以前代码的重构与优化

    对于Ext.data.Store 一直不是很了解,不知道他到底是干嘛的有哪些用处,在实际开发中也由于不了解也走了不少弯路, store是一个为Ext器件提供record对象的存储容器,行为和属性都很象 ...

  6. XML部分

    XML文档定义有几种形式?它们之间有何本质区别?解析XML文档有哪几种方式? 两种形式:DTD以及schema: 本质区别:schema本身是xml的,可以被XML解析器解析(这也是从DTD上发展sc ...

  7. [转]group by 后使用 rollup 子句总结

    group by 后使用 rollup 子句总结 一.如何理解group by 后带 rollup 子句所产生的效果 group by 后带 rollup 子句的功能可以理解为:先按一定的规则产生多种 ...

  8. url字符长度限制解决办法

    前段时间,同事往系统上传相关文档,发现输入失败,找到了我了. 开始以为数据库字段属性问题,修改后未解决随调试系统,发现没有走到后台程序,发现 ajax没有传值,各种测试问题情况,后来同事发现是url字 ...

  9. 2、买卖股票的最佳时机 II

    2.买卖股票的最佳时机 II 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格. 设计一个算法来计算你所能获取的最大利润.你可以尽可能地完成更多的交易(多次买卖一支股票). 注意:你不能 ...

  10. 设计模式之适配器模式(Adapter)(6)

    简介 在实际的开发过程中,由于应用环境的变化(例如使用语言的变化),我们需要的实现在新的环境中没有现存对象可以满足,但是其他环境却存在这样现存的对象.那么如果将“将现存的对象”在新的环境中进行调用呢? ...