前面的话

  学习如何创建对象是理解面向对象编程的第一步,第二步是理解继承。开宗明义,继承是指在原有对象的基础上,略作修改,得到一个新的对象。javascript主要包括类式继承、原型继承和拷贝继承这三种继承方式。本文是javascript面向对象系列第三篇——实现继承的3种形式

类式继承

  大多数面向对象的编程语言都支持类和类继承的特性,而JS却不支持这些特性,只能通过其他方法定义并关联多个相似的对象,如new和instanceof。不过在后来的ES6中新增了一些元素,比如class关键字,但这并不意味着javascript中是有类的,class只是构造函数的语法糖而已

  类式继承的主要思路是,通过构造函数实例化对象,通过原型链将实例对象关联起来。下面将对类式继承进行详细解释

【原型链继承】

  javascript使用原型链作为实现继承的主要方法,实现的本质是重写原型对象,代之以一个新类型的实例。下面的代码中,原来存在于SuperType的实例对象中的属性和方法,现在也存在于SubType.prototype中了

function Super(){this.value = true;
}
Super.prototype.getValue = function(){return this.value;
};
function Sub(){}
//Sub继承了Super
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;var instance = new Sub();
console.log(instance.getValue());//true

  原型链最主要的问题在于包含引用类型值的原型属性会被所有实例共享,而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了

function Super(){this.colors = ['red','blue','green'];
}
function Sub(){};
//Sub继承了Super
Sub.prototype = new Super();
var instance1 = new Sub();
instance1.colors.push('black');
console.log(instance1.colors);//'red,blue,green,black'
var instance2 = new Sub();
console.log(instance2.colors);//'red,blue,green,black'

  原型链的第二个问题是,在创建子类型的实例时, 不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。再加上包含引用类型值的原型属性会被所有实例共享的问题,在实践中很少会单独使用原型链继承

【借用构造函数继承】

  借用构造函数(constructor stealing)的技术(有时候也叫做伪类继承或经典继承)。基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数,通过使用apply()和call()方法在新创建的对象上执行构造函数

function Super(){this.colors = ['red','blue','green'];
}
function Sub(){//继承了SuperSuper.call(this);
}
var instance1 = new Sub();
instance1.colors.push('black');
console.log(instance1.colors);// ['red','blue','green','black']
var instance2 = new Sub();
console.log(instance2.colors);// ['red','blue','green']

  相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数

function Super(name){this.name = name;
}
function Sub(){//继承了Super,同时还传递了参数Super.call(this,"bai");//实例属性this.age = 29;
}
var instance = new Sub();
console.log(instance.name);//"bai"
console.log(instance.age);//29  

  但是,如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了

【组合继承】

  组合继承(combination inheritance)有时也叫伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性

function Super(name){this.name = name;this.colors = ['red','blue','green'];
}
Super.prototype.sayName = function(){console.log(this.name);
};
function Sub(name,age){//继承属性Super.call(this,name);this.age = age;
}
//继承方法
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function(){console.log(this.age);
}
var instance1 = new Sub("bai",29);
instance1.colors.push("black");
console.log(instance1.colors);//['red','blue','green','black']
instance1.sayName();//"bai"
instance1.sayAge();//29var instance2 = new Sub("hu",27);
console.log(instance2.colors);//['red','blue','green']
instance2.sayName();//"hu"
instance2.sayAge();//27

  组合继承有它自己的问题。那就是无论什么情况下,都会调用两次父类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。子类型最终会包含父类型对象的全部实例属性,但不得不在调用子类型构造函数时重写这些属性

function Super(name){this.name = name;this.colors = ["red","blue","green"];
}
Super.prototype.sayName = function(){return this.name;
};
function Sub(name,age){// 第二次调用Super(),Sub.prototype又得到了name和colors两个属性,并对上次得到的属性值进行了覆盖Super.call(this,name);this.age = age;
}
//第一次调用Super(),Sub.prototype得到了name和colors两个属性
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function(){return this.age;
};  

【寄生组合继承】

  解决两次调用的方法是使用寄生组合式继承。寄生组合式继承与组合继承相似,都是通过借用构造函数来继承不可共享的属性,通过原型链的混成形式来继承方法和可共享的属性。只不过把原型继承的形式变成了寄生式继承。使用寄生组合式继承可以不必为了指定子类型的原型而调用父类型的构造函数,从而寄生式继承只继承了父类型的原型属性,而父类型的实例属性是通过借用构造函数的方式来得到的

  [注意]下方中会对寄生继承进行详细说明

function Super(name){this.name = name;this.colors = ["red","blue","green"];
}
Super.prototype.sayName = function(){return this.name;
};function Sub(name,age){Super.call(this,name);this.age = age;
}
if(!Object.create){Object.create = function(proto){function F(){};F.prototype = proto;return new F;}
}
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;var instance1 = new Sub("bai",29);
instance1.colors.push("black");
console.log(instance1.colors);//['red','blue','green','black']
instance1.sayName();//"bai"var instance2 = new Sub("hu",27);
console.log(instance2.colors);//['red','blue','green']
instance2.sayName();//"hu"

  这个例子的高效率体现在它只调用了一次Super构造函数,并且因此避免了在Sub.prototype上面创建不必要的、多余的属性。与此同时,原型链还保持不变

  因此,开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式,YUI的YAHOO.lang.extend()方法就采用了这种继承模式

【ES6中的class】

  如果使用ES6中的class语法,则上面代码修改如下

  [注意]关于关于ES6中的class语法,详细情况移步至此

class Super {constructor(name){this.name = name;this.colors = ["red","blue","green"];}sayName(){return this.name;}
}class Sub extends Super{constructor(name,age){super(name);this.age = age;}
}var instance1 = new Sub("bai",29);
instance1.colors.push("black");
console.log(instance1.colors);//['red','blue','green','black']
instance1.sayName();//"bai"var instance2 = new Sub("hu",27);
console.log(instance2.colors);//['red','blue','green']
instance2.sayName();//"hu"

  ES6的class语法糖隐藏了许多技术细节,在实现同样功能的前提下,代码却优雅不少

原型继承

【原型继承】

  原型继承,在《你不知道的javascript》中被翻译为委托继承

  道格拉斯·克罗克福德(Douglas Crockford)在2006年写了一篇文章,《javascript中的原型式继承》。在这篇文章中,他介绍了一种实现继承的方式,这种方式并没有使用严格意义上的构造函数。他的想法是借助原型可以基于已有的对象来创建新对象,同时不必因此创建自定义类型

  原型继承的基础函数如下所示

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

  在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制

  下面是一个例子

var superObj = {init: function(value){this.value = value;},getValue: function(){return this.value;}
}var subObj = object(superObj);
subObj.init('sub');
console.log(subObj.getValue());//'sub'

  ES5通过新增Object.create()方法规范化了原型式继承

  [注意]关于Object.create()方法的详细内容移步至此

var superObj = {init: function(value){this.value = value;},getValue: function(){return this.value;}
}var subObj = Object.create(superObj);
subObj.init('sub');
console.log(subObj.getValue());//'sub'

【与原型链继承的关系】

  原型继承虽然只是看上去将原型链继承的一些程序性步骤包裹在函数里而已。但是,它们的一个重要区别是父类型的实例对象不再作为子类型的原型对象

  1、使用原型链继承

function Super(){this.value = 1;
}
Super.prototype.value = 0;
function Sub(){};
//将父类型的实例对象作为子类型的原型对象
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;//创建子类型的实例对象
var instance = new Sub;
console.log(instance.value);//1

  2、使用原型继承

function Super(){this.value = 1;
}
Super.prototype.value = 0;
function Sub(){};Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;//创建子类型的实例对象
var instance = new Sub;
console.log(instance.value);//0

  上面的Object.create函数一行代码Sub.prototype = Object.create(Super.prototype)可以分解为

function F(){};
F.prototype = Super.prototype;
Sub.prototype = new F();

  由上面代码看出,子类的原型对象是临时类F的实例对象,而临时类F的原型对象又指向父类的原型对象;所以,实际上,子类可以继承父类的原型上的属性,但不可以继承父类的实例上的属性

  原型继承与原型链继承都存在着子例共享父例引用类型值的问题

var superObj = {colors: ['red','blue','green']
};
var subObj1 = object(superObj);
subObj1.colors.push("black");var subObj2 = object(superObj);
subObj2.colors.push("white");console.log(superObj.colors);//["red", "blue", "green", "black", "white"]
console.log(subObj1.colors);//["red", "blue", "green", "black", "white"]

【寄生式继承】

  寄生式继承(parasitic)是与原型继承紧密相关的一种思路,并且同样是由道格拉斯·克罗克福德推而广之的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后再返回对象

function parasite(original){var clone = Object.create(original);//通过调用函数创建一个新对象clone.sayHi = function(){ //以某种方式来增强这个对象console.log("hi");};return clone;//返回这个对象
}
var superObj = {colors: ['red','blue','green']
};
var subObj1 = parasite(superObj);
subObj1.colors.push('black');
var subObj2 = parasite(superObj);
subObj2.colors.push('white');console.log(superObj.colors);//["red", "blue", "green", "black", "white"]
console.log(subObj1.colors);//["red", "blue", "green", "black", "white"]

  由于原型继承存在着引用类型的值被共享的问题,所以使用得并不很多,只在一些简单应用场景下使用。如果需要解决该问题,则需要借用构造函数,与原型继承的初衷相违背,相当于使用了类式继承的终极写法——寄生组合继承

拷贝继承

  拷贝继承在《javascript面向对象摘要》中翻译为混入继承,jQuery使用的就是拷贝继承

  拷贝继承不需要改变原型链,通过拷贝函数将父例的属性和方法拷贝到子例即可

  [注意]关于对象拷贝的详细信息移步至此

【拷贝函数】

  下面是一个深拷贝的拷贝函数

function extend(obj,cloneObj){if(typeof obj != 'object'){return false;}var cloneObj = cloneObj || {};for(var i in obj){if(typeof obj[i] === 'object'){cloneObj[i] = (obj[i] instanceof Array) ? [] : {};arguments.callee(obj[i],cloneObj[i]);}else{cloneObj[i] = obj[i]; }  }return cloneObj;
}var obj1={a:1,b:2,c:[1,2,3]};
var obj2=extend(obj1);
console.log(obj1.c); //[1,2,3]
console.log(obj2.c); //[1,2,3]
obj2.c.push(4);
console.log(obj2.c); //[1,2,3,4]
console.log(obj1.c); //[1,2,3]

【对象间的拷贝继承】

  由于拷贝继承解决了引用类型值共享的问题,所以其完全可以脱离构造函数实现对象间的继承

function extend(obj,cloneObj){if(typeof obj != 'object'){return false;}var cloneObj = cloneObj || {};for(var i in obj){if(typeof obj[i] === 'object'){cloneObj[i] = (obj[i] instanceof Array) ? [] : {};arguments.callee(obj[i],cloneObj[i]);}else{cloneObj[i] = obj[i]; }  }return cloneObj;
}var superObj = {arrayValue:[1,2,3],init: function(value){this.value = value;},getValue: function(){return this.value;}
}
var subObj = extend(superObj);
subObj.arrayValue.push(4);
console.log(subObj.arrayValue);//[1,2,3,4]
console.log(superObj.arrayValue);//[1,2,3]

【使用构造函数的拷贝组合继承】

  如果要使用构造函数,则属性可以使用借用构造函数的方法,而引用类型属性和方法使用拷贝继承。相当于不再通过原型链来建立对象之间的联系,而通过复制来得到对象的属性和方法

function extend(obj,cloneObj){if(typeof obj != 'object'){return false;}var cloneObj = cloneObj || {};for(var i in obj){if(typeof obj[i] === 'object'){cloneObj[i] = (obj[i] instanceof Array) ? [] : {};arguments.callee(obj[i],cloneObj[i]);}else{cloneObj[i] = obj[i]; }  }return cloneObj;
}
function Super(name){this.name = name;this.colors = ["red","blue","green"];
}
Super.prototype.sayName = function(){return this.name;
};
function Sub(name,age){Super.call(this,name);this.age = age;
}
Sub.prototype = extend(Super.prototype);
var instance1 = new Sub("bai",29);
instance1.colors.push("black");
console.log(instance1.colors);//['red','blue','green','black']
instance1.sayName();//"bai"var instance2 = new Sub("hu",27);
console.log(instance2.colors);//['red','blue','green']
instance2.sayName();//"hu"

总结

  本文介绍的类式继承、原型继承和拷贝继承这三种继承方式中,类式继承用的最普遍,由于ES6中的class的语法糖,使其代码复杂度大大降低;原型继承由于无法处理引用类型值共享的问题,使用较少,但由原型继承引申出的寄生组合继承是类式继承的范式方法;拷贝继承使用范围最广泛,不仅可以实现原型之间的继承,也可以脱离构造函数,直接实现对象间的继承

  总之,继承主要就是处理父例和子例之间的两个问题,即是否使用构造函数,及如何建立联系

  类式继承的核心就是使用构造函数,通过原型链来建立联系

  原型继承不使用构造函数,通过Object.create()来建立联系

  拷贝继承使不使用构造函数都可以,通过复制来建立联系

javascript面向对象系列第三篇——实现继承的3种形式相关推荐

  1. 深入理解javascript作用域系列第三篇

    前面的话 一般认为,javascript代码在执行时是由上到下一行一行执行的.但实际上这并不完全正确,主要是因为声明提升的存在.本文是深入理解javascript作用域系列第三篇--声明提升(hois ...

  2. JavaScript面向对象编程-第三版不完全系统解读

    JavaScript面向对象编程-第三版不完全系统解读 作者:老九-技术大黍 产品:查看原文 社交:知乎 公众号:老九学堂(新手有福利) 特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系笔者 ...

  3. [深度][PyTorch] DDP系列第三篇:实战与技巧

    [深度][PyTorch] DDP系列第三篇:实战与技巧 转自:https://zhuanlan.zhihu.com/p/250471767 零. 概览 想要让你的PyTorch神经网络在多卡环境上跑 ...

  4. 深入理解javascript作用域系列第四篇——块作用域

    前面的话 尽管函数作用域是最常见的作用域单元,也是现行大多数javascript最普遍的设计方法,但其他类型的作用域单元也是存在的,并且通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀.简洁的 ...

  5. 【JavaScript 教程系列第 10 篇】判断一个数是整数还是小数

    这是[JavaScript 教程系列第 10 篇],如果觉得有用的话,欢迎关注专栏. 思路 如果一个数是整数,那么 parseInt() 函数和 parseFloat() 函数的返回值是相同的,反之返 ...

  6. 【JavaScript 教程系列第 8 篇】什么是闰年?判断某一年是不是闰年

    这是[JavaScript 教程系列第 8 篇],如果觉得有用的话,欢迎关注专栏. 闰年分为 普通年:能被 4 整除且不能被 100 整除的是闰年. 世纪年:能被 400 整除的是闰年. 依据这两个判 ...

  7. javascript面向对象系列第一篇——构造函数和原型对象

    前面的话 一般地,javascript使用构造函数和原型对象来进行面向对象编程,它们的表现与其他面向对象编程语言中的类相似又不同.本文将详细介绍如何用构造函数和原型对象来创建对象 构造函数 构造函数是 ...

  8. JavaScript 面向对象编程(三) —— 函数进阶 / 严格模式 / 高阶函数 / 闭包 / 浅拷贝和深拷贝

    本篇为 JavaScript 进阶 ES6 系列笔记第三篇,将陆续更新后续内容.参考:JavaScript 进阶面向对象 ES6 :ECMAScript 6 入门 系列笔记: JavaScript 面 ...

  9. javascript面向对象学习笔记(一)——继承

    最近在学习html5,玩了下canvas,发现js中很多的东西都不太记得了.翻了下笔记后发现还是去图书馆逛逛把,到借阅区找了我一直想看的<javascript design patterns&g ...

最新文章

  1. Java EE---Spring AOP
  2. nginx 配置信息
  3. Hystrix 熔断器01—— 概述 || Hystrix 重要概念
  4. 10分钟搞定 Java 并发队列好吗?好的
  5. LintCode: Single Number II
  6. mysql存储过程报错_MySQL存储过程错误No data - zero rows fetched, selected, or processed
  7. atom自动补全html代码,Atom - Emmet插件的使用详解(HTML/CSS代码自动补全)
  8. 好文推荐系列--------(3)GruntJS 在线重载 提升生产率至新境界
  9. pg数据库开启远程连接_疫情之下,开启在家办公模式,远程连接工具篇之向日葵...
  10. python读取两个csv文件后比较_python – 读取两个csv文件并比较每一行.如果行匹配打印两行,如果不相似则打印无效...
  11. dfa算法c语言,DFA跟trie字典树实现敏感词过滤(python和c语言)
  12. 在HP-UX上安装Oracle11.2.0.3修改maxfiles
  13. Java设计模式——代理模式实现及原理
  14. 安装MiniTools后,不能识别USB的问题
  15. xmpp即时通讯协议的特性---长处和缺点!
  16. java串口监控数据,怎样监听或者拦截串口上的数据
  17. C语言,C++,C#发展的回顾与展望
  18. 01、u3d自学课程
  19. 我们为什么来到这个世界上?--黄金雄(杰西)
  20. FC200 长广防盗维修专家

热门文章

  1. Graphic系统综合练习案例-绘制饼状图
  2. iOS 中 #import同@class之间的区别
  3. IE10 URL中多出一串字符,图片无法显示
  4. linux之rpm命令
  5. 网络基础四 DNS DHCP 路由 FTP
  6. html中元素盒子垂直居中的实现方法
  7. 聊聊ES7与ES8特性
  8. 数位DP按位枚举模板
  9. 4--RESTful应用程序
  10. features its own