细谈Javascript中的继承以及各种继承的类型
Posted 我是真的不会前端
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了细谈Javascript中的继承以及各种继承的类型相关的知识,希望对你有一定的参考价值。
1.继承的概念
先说下原型链
原型链是实现继承的主要方法。构造函数都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针(constructor),而任何实例或者实例化对象都包含一个指向原型对象的内部指针(proto)。假如我们让原型对象A等于另一个类型B的实例,那么原型对象A就包含一个指向另一个类型B的原型对象的指针,以此类推,B的原型对象又是C类型的实例,最终指向object。上面的关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是原型链的概念。
如图所示
首先字面意思上简单理解就是儿子继承爸爸的特性
function Animal(name,age){
this.name = name;
this.age = age
}
Animal.prototype.say = function(){
console.log('各种声音')
}
function Cat(eye) {
this.eye = eye
}
Cat.prototype.xingwei = function () {
console.log('抓老鼠')
}
function Bird(body) {
this.body = body
}
Bird.prototype.fly = function () {
console.log('飞')
}
const c = new Cat('布偶')
console.log(c)
const b = new Bird('翅膀')
console.log(b)
从下面两个实例对象中可以看出,每个对象都有自己的属性和方法,不带有Animal的属性和方法,但Cat和Bird又希望能使用到Animal的属性和方法。
当多个构造函数需要使用一些共同的属性和方法时候,就需要将将共同的属性和方法单独封装在一个构造函数中,方便多个构造函数继承使用。
代码中的继承,其实就是让一个对象可以拥有另一个对象的属性和方法
多个对象由共同的属性和方法,就将共同的东西抽取出来进行封装并重复使用
父构造函数叫父类,子构造函数叫子类。继承的目的,就是让子类拥有父类的属性和方法。
2.原型继承
JS找构造函数的属性的时候,当找不到时就会去原型_PROTO_找。找不到再去原型的原型找。知道搜索完整个原型链。说白了原型的继承就是改原型链
通过改对象的原型链结构来实现继承,因为对象默认能访问到原型对象的属性和方法
让构造函数换一个原型。
原型继承:利用构造函数可以修改实例对象的原型,将原型换成想继承的那个对象。
function Animal(name,age){
this.name = name;
this.age = age
}
Animal.prototype.say = function(){
console.log("各种声音");
}
// var a = new Animal('动物',5)
// console.log(a);
function Bird(skill){
this.skill = skill
}
结果是:
// Bird.prototype = a
Bird.prototype = new Animal('动物',5)
var b = new Bird('能飞')
// 将b的原型改成Animal的实例对象a
// b.__proto__ = a
console.log(b);
console.log(b.skill);
b.say()
优点:只要处在原型上的方法和属性都能使用
缺点:需要在两个地方传参,所有属性分另个地方存放
3.借用函数继承
借用函数(上下文调用模式):call apply bind
先说下这三个的区别:
call apply调用函数并改变其this指向
bind克隆函数并改变其this指向
利用借用函数,将父构造函数中的this改成子构造函数中的this,让子构造函数具备跟父构造函数同样的属性。
function Animal(name,age){
this.name = name;
this.age = age
}
Animal.prototype.say = function(){
console.log("各种声音");
}
function Bird(skill,name,age){
this.skill = skill
// 这里执行Animal构造函数中的代码
// Animal() // 直接执行,这里的this - 代表Bird的实例对象,就没有name、age
// var that = this;
// Animal.call(this,name,age)
Animal.apply(this,[name,age])
}
现在我们来看结果
优点:解决了参数多个地方传参,存储属性在多个地方存储的问题
问题:无法继承父构造函数原型上的方法
4.混合继承
继承的方案也是个渐进的过程,直到找到最完美的方案。就跟回调函数的解决从promise的引入到async和await的方法,具体内容可以看我之前写的有关回调地狱的解决方案文章:回调地狱的终极解决方案
好了 回到正题:其实顾名思义,就是上面两种方法的混合。是将原型继承一次,借用函数再继承一次
function Animal(name,age){
this.name = name;
this.age = age
}
Animal.prototype.say = function(){
console.log("各种声音");
}
function Bird(skill,name,age){
this.skill = skill
// 使用借用函数继承一次
Animal.apply(this,[name,age])
}
// 使用原型再继承一次
Bird.prototype = new Animal()
// 给子类添加方法
Bird.prototype.fly = function(){
console.log("飞的更高");
}
var b = new Bird('能飞','鸟',2)
console.log(b);
function Cat(name,age){
this.name = name;
this.age = age
}
Cat.prototype = new Animal()
var c = new Cat('猫',3)
console.log(c);
来看结果
看到这 不用我说,就能看到缺陷吧
优点:在同一个地方传递参数了,且属性都放在了一起。
缺点:父构造函数原型上的方法继承不了。
5.拷贝继承
拷贝继承是利用for in循环能将原型中的属性和方法也遍历出来的特性,将父对象中属性和方法遍历绑定到子对象的原型上。
也可以说将父类的属性拷贝到子类中。
function Animal(name,age){
this.name = name;
this.age = age
}
Animal.prototype.say = function(){
console.log("各种声音");
}
function Bird(name,age){
// 需要进行拷贝父类中的属性和方法
// for in 循环 - 能将原型中的东西也遍历
var a = new Animal(name,age)
console.log(a);
for(var attr in a){
this.__proto__[attr] = a[attr]
}
}
Bird.prototype.fly = function(){
console.log("飞得更高");
}
var b = new Bird('鸟',2)
console.log(b);
console.log(b.name);
优点:
- 父类的构造函数体内的和原型上的都可以继承
- constructor 能正常配套
- 添加自己的方法的时候, 确实是在自己的原型身上
缺点:
- for in 循环: for in 循环需要一直遍历到 Object.prototype - 比较消耗性能
- 不能继承 不可枚举 的属性 - 颜色比较暗的属性和方法不能遍历到
- 继承来的属性不再自己身上
6.寄生继承
将父构造函数在子构造函数中实例化,然后在子构造函数中返回实例出来的对象。寄生继承写起来简单,但是不是很负责任。把子类实例化的类型返回出来。得到父对象。然后再子类将父类实例化的操作返回出来。继承出来的对象等于是把自己变成了父亲。给自己添加属性没有用。只能加到父类上去。等同于迷失了自我。
function Animal(name,age){
this.name = name;
this.age = age;
}
Animal.prototype.say = function(){
console.log("各种声音");
}
function Bird(name,age,skill){
// 给自己添加属性是没有用的,因为最终返回的不是this
this.skill = skill
// 在子类这个地方实例化父类得到一个对象
var a = new Animal(name,age)
// 在子类中将父类实例化出来的对象返回
return a
}
结果
这种继承没有了自己的原型,原型完全是父构造函数的继承,改变自己原型后,自己原本原型上的方法又无法使用。
那么这个时候 ,就产生了改进方法。
我们可以依然new父元素。我们将父原型绑在自己身上。
function Animal(name,age){
this.name = name;
this.age = age;
}
Animal.prototype.say = function(){
console.log("各种声音");
}
function Bird(name,age,skill) {
// 在子构造函数中实例化父构造函数
var a = new Animal(name,age)
// 改原型
Bird.prototype = a.__proto__;
// 给这个对象中添加属性
a.skill = skill;
return a;
}
Animal.prototype.fly = function(){
console.log("飞得更高");
}
var b = Bird('鸟',2,'眼睛')
console.log(b) // 对象中又有了xingwei这个方法了
结果如下
但这种继承方式,将父构造函数的原型修改了,以后的继承,都会带有这个方法。但缺点还是有。就是容易产生冲突
优点:
- 原型和构造函数体内的都能继承下来
- 寄生原型的话, 自己的属性和方法依旧可以添加和使用
缺点:
- 寄生实例的时候, 没有自己的任何内容
- 寄生原型的时候, 一旦修改原型上, 父类的实例也会有这些方法
7.寄生组合继承
到现在为止,也称完美继承
进行一次寄生,再进行一次组合(原型+借用)。实际上三次继承结合到了一起。同时再加上独立的第三方构造函数。那怎么理解这个第三方呢。其实你可以理解为他是一个中间人。借助寄生和借用他拿取相应的属性。
function Animal(name,age){
this.name = name
this.age = age
}
Animal.prototype.say = function(){
console.log("各种声音");
}
function Temp(name,age){
// 通过借用函数继承父类
Animal.call(this,name,age)
}
// 原型继承
Temp.prototype = new Animal()
// 实例化第三方类
var t = new Temp('动物',5)
// 原型继承
Bird.prototype = t
function Bird(skill){
this.skill = skill
}
Bird.prototype.fly = function(){
console.log("飞得更高");
}
var b = new Bird('会飞')
console.log(b);
过程如下:
结果如下
Animal经过借用函数继承后,属性已经有了,需要的只剩下原型中的方法了,所以对原型进行寄生即可,不用实例化Animal了,简化如下:
(function(){
// 寄生组合继承:寄生继承 + 原型继承 + 借用函数继承 + 独立的第三方构造函数
function Temp(name,age){
// 通过借用函数继承父类
Animal.call(this,name,age)
}
// 寄生原型
Temp.prototype = Animal.prototype
// 实例化第三方类
// var t = new Temp('动物',5)
// 原型继承
Bird.prototype = new Temp('动物',5)
})()
// console.log(Temp);
function Bird(skill){
this.skill = skill
}
Bird.prototype.fly = function(){
console.log("飞得更高");
}
var b = new Bird('能飞')
console.log(b);
自执行函数防止变量污染。通过寄生原型直接写成Animal.prototype。
控制台结果如下
8.ES6中的继承
更完美的继承方案。通过class 和extends。父级的constructer需要用super来调用:
super关键字代表的就是表示通过子类的构造方法调用父类的构造方法。也可以说“当前对象”的那部分父类型特征。
super() 是调用父类的构造函数super的语法是:“super.”、“super()”。
class Animal{
constructor(name,age){
this.name = name
this.age = age
}
say(){
console.log("各种声音");
}
}
class Bird extends Animal{
constructor(name,age){
super(name,age)
}
fly(){
console.log("飞的更高");
}
}
var b = new Bird('鸟',2)
console.log(b);
看这次的继承结果,是不是很完美
9.总结
说一千道一万。对于继承这件事情。无论从单纯的原型继承还是到现在ES6最完美的解决方案class+extends继承。从来都围绕不开原型。js这种弱类型本身没有面向对象。布兰登艾奇只是发明了原型来 模拟后端语言的面向对象和继承。说一千道一万,这么多种方法的改进更迭都离不开原型。JS也没有类,他用构造函数完成了类。JS本身也没有继承。同样通过原型和原型链,完成了继承。这样才实现了JS的更多高级应用。
以上是关于细谈Javascript中的继承以及各种继承的类型的主要内容,如果未能解决你的问题,请参考以下文章
JavaScript实现类的privateprotectedpublicstatic以及继承