JavaScript面向对象之图解原型链继承的原理与问题及解决办法

191 阅读14分钟

JavaScript面向对象之图解原型链继承的原理

因为牵扯到原型链最好是画出对应的关系,通过原型链来找出它们的这个关系,更好的能理解它所实现的本质,就是重写了原型对象。

以这个代码为例:

function Animal(){   //这是定制我们当前的属性 
 //定义一个能共享的名字
 this.name="小黄"
 }
Animal.prototype.getName=function(){
    return this.name;
}

function Dog(){};
//Dog继承了Animal
Dog.prototype=new Animal();  //改变原型对象
Dog.prototype.constructor=Dog;  //改变它的构造函数
var d1=new Dog();//实例化出来
console.log(d1.name)
console.log(d1.getName())

上面我们有一个构造函数Animal,这个构造函数我们说它是一个对象,实例对象是new Animal();也是个对象,这样的话Animal是一个构造函数,那么构造函数的原型Animal.protopyte也是一个对象,那接下我们画一张图来分析一下

第一步

function Animal(){   //这是定制我们当前的属性 
 //定义一个能共享的名字
 this.name="小黄"
 }
Animal.prototype.getName=function(){
    return this.name;
}

来一个构造函数Animal,一个构造函数中应该有它储存的内容this.name="小黄",并且还储存了prototype这么一个属性,Animal指向了当前实例的__proto__,这个prototype是一个原型对象,那么prototype也是一个对象,此时再写一个Animal.prototype,那么这是一个原型对象,同时也可以把它当做一个实例对象,那么它的内部有一个constructor,这个constructor就指向了Animal,并且如果把它称作一个实例对象的话,它这里面也有它自己的__proto__,那它就指向我们另一个函数的prototype属性就是Object,所有的实例都继承了Object.prototype,这个原型对象上还存储了一个值,这个值就是getName它对应的是一个函数。

第二步

function Dog(){};
//Dog继承了Animal
Dog.prototype=new Animal();  //改变原型对象
Dog.prototype.constructor=Dog;  //改变它的构造函数

首先有一个构造函数Dog,这个Dog中没有存储其他东西,但是它自己内部有一个prototype属性,它的prototype指向了Dog实例的这个__proto__,在这个时候我们改变了Dogprototype。这个原型对象,改变成new Animal();

new Animal();也是一个实例对象,假设我们以new Animal();来一个实例var a= new Animal();,然后我们实例出来以后会有它自己的__proto__,那么它的这个实例指向了Animal的原型,那么这个时候Animal原型上的constructorgetName都归当前实例出来的Animal a所有,并且当我们new Animal的时候,在上面有一个name赋值将小黄赋值给name,也就是说在当前new Animal a实例里面它还存储了一个属性name:小黄,那么它的这个方法是属于Animal原型的方法是一个共享的方法。这时候当代码执行到Dog.prototype=new Animal(); 将当前的new Animal();实例赋值给当前的原型Dog.prototype,也就意味着当前的原型指向当前a这个实例,代码接下来执行就是把Dog的原型的constructor改变成当前的Dog,我们原来的constructor是指向了Animal,如果我们var aconstructor也是指向了当前的Animal,现在var a的实例里面除了有name属性,还有一个constructor属性,这个constructor也指向了Animal,此时的Animal是继承来的。现在我的代码一执行Dog.prototype.constructor=Dog;,改变了原型对象的constructor,也就意味着Animal被重写成Dog了。以后的每一个实例都是归Dog构造函数构造出来的。

第三步

var d1=new Dog();//实例化出来
console.log(d1.name)
console.log(d1.getName())

有这么一个实例对象var d1 = new Dog();,这个实例对象中有它自己的constructor属性,那这个constructor属性指向了当前Dog构造函数,因为Dog原型发生了改变都是var a = new Animal();,所以var d1constructor指向Dog,此时的Dog是继承来的,var d1不止有prototype一个属性,里面还有一个__proto__,它的指向是当前Dog.prototype

由这个原型链的关系,我们来看一下当前d1实例中的getName和name属性现在分别属于谁,首先看name属性,现在var a = new Animal();这个实例赋值给了当前Dog.prototype,所以当前d1.name获取出来的小黄应该属于我们Dog的原型,然后getName现在还是仍然在当前的Animal的原型对象上,所以说getName方法在当前的Animal的原型上。

打印一下d1,这里面没有属性和方法,但是在d1的原型上指向了Animal,它这里面定制了一个Animal对象,name属性位于当前Dog的原型上,那么我们的方法getName属于当前父类Animal的原型 通过原型链就能看出来我们重写了我们当前Dog的一个原型,把它改变成一个Animal的实例,这样的话里面的name属性getName方法都能共享,这就是继承。

通过d1.name能拿到小黄。通过d1.getName();也能拿到小黄

原型链继承存在的问题

首先我们要清除的知道,在我们修改原型的时候一定要修改它的constructor指向。

问题一

当前父类构造函数(Animal)中声明的这个私有原型上的属性,都会被实例所共享。 比如说在去定制一个颜色color ,color是一个引用类型的数组。再去创建一个实例的时候var d2=new Dog;现在d1d2都会共有这个数组。

function Animal(){   //这是定制我们当前的属性 
 //定义一个能共享的名字
 this.name="小黄";
 this.color=['reg',"green",'blue']
 }
Animal.prototype.getName=function(){
    return this.name;
}

function Dog(){};
//Dog继承了Animal
Dog.prototype=new Animal();  //改变原型对象
Dog.prototype.constructor=Dog;  //改变它的构造函数
var d1=new Dog();//实例化出来
var d2=new Dog();
console.log(d1.color)
console.log(d2.color)

父类中的实例属性一旦赋值给子类的原型属性,此时这些属性都属于子类的共享属性

这个时候我给d2再添加一个颜色 d2.color.push("purple"),那么它也会共享到d1,因为这个实例属性中的color会被共享。

function Animal(){   //这是定制我们当前的属性 
 //定义一个能共享的名字
 this.name="小黄";
 this.color=['reg',"green",'blue']
 }
Animal.prototype.getName=function(){
    return this.name;
}

function Dog(){};
//Dog继承了Animal
Dog.prototype=new Animal();  //改变原型对象
Dog.prototype.constructor=Dog;  //改变它的构造函数
var d1=new Dog();//实例化出来
var d2=new Dog();
console.log(d1.color)
console.log(d2.color)
d1.color.push("purple")
console.log(d1.color)
console.log(d2.color)

因为在 new Animal当做一个实例赋值给了Dog.prototype,于是我们原先的这个Animal函数中的这些实例属性也就成了当前这个原型(Dong.prototype)的属性,这个new Animal();namecolor作为一个实例属性存在,但是现在一旦赋值给Dog.prototype之后,此时就称为Dog中的一个共享属性。

问题二

在我们实例化子类型实例的时候new Dog();不能向父类型传值

var d2=new Dog("小黄");   //不能这样传值

也就由于这两个问题,我们在实际开发中不用原型链继承来开发。

借用构造函数继承

借用构造函数就是要想创建子类与父类有两个构造函数,在子类中我们可以调用父类中的函数来进行实现继承,这种继承也称作伪类继承与经典继承,在子类的构造函数内部去调用父类的构造函数。

function Animal(){   //这是定制我们当前的属性 
 //定义一个能共享的名字
 this.name="小黄";
 this.color=['reg',"green",'blue']
 }
Animal.prototype.getName=function(){
    return this.name;
}
function Dog(){
	//Animal();    //如果这样调用时没有用的,与父类构造函数没有任何关联
// 继承了Animal
    Animal.call(this)
}
var d1= new Dog();
console.log(d1.name)
console.log(d1.color)

首先创建一个父类的构造函数Animal,再创建一个子类的构造函数Dog。在子类构造函数内部中中去调用父类构造函数,如果单纯的使用Animal();调用是没有用的。我们之前在学函数调用的时候有学到间接式调用。间接式调用有call();apply();bind();,这里通过call();方法进行调用,把当前内部的this传递进去,当传递进去。然后在下面去new一个实例d1,这样的话就属于借用构造函数去调用父类的构造函数,这就实现了继承

内部实现原理:每次调用new Dog函数的时候,构造函数中的this会发生改变,Animal.call(this)指向了d1对象。当调用Animal构造函数的时候里面的this相当于把d1传递到Animal的构造函数内部,当前内部的this就是d1

借用函数继承解决的问题

1、解决了父类中的实例属性一旦赋值给子类的原型属性,此时这些属性都属于子类的共享属性的问题

通过借用构造函数继承,仅仅单纯继承了当前父类构造函数中的属性,并且解决了前面出现的父类中添加属性一旦重写,子类的原型对象就会全部共享继承。

function Animal(){   //这是定制我们当前的属性 
 //定义一个能共享的名字
 this.name="小黄";
 this.color=['reg',"green",'blue']
 }
Animal.prototype.getName=function(){
    return this.name;
}
function Dog(){
    // 继承了Animal
    Animal.call(this)
}
var d1= new Dog();
var d2 =new Dog();
d2.color.push("purple")
console.log(d1.name)
console.log(d1.color)
console.log(d2.color)

父类构造函数中的实例属性也作为子类构造函数Dog的实例属性,所以这些属性没有被共享下来

2、解决实例化子类型的时候,不能向父类构造函数传参的问题

现在我们实例化一个函数的时候是可以传参的,所以在父类的构造函数中声明一些引用类型的数据类型,那么每个子类都有它自己的实例属性

function Animal(name){   //这是定制我们当前的属性 
 //定义一个能共享的名字
 this.name=name;
 this.color=['reg',"green",'blue']
 }
Animal.prototype.getName=function(){
    return this.name;
}
function Dog(name){
    // 继承了Animal
    Animal.call(this,name)
}
var d1= new Dog("小胖");
var d2 =new Dog("小太阳");
d2.color.push("purple")
console.log(d1.name)
console.log(d1.color)
console.log(d2.color)

借用构造函数继承的缺点

但是它存在的缺点就是父类中定义的共享的方法不能被共享下来

call()在本文中的作用

call()是一个间接式调用。在本文中的内部机制就是,当new实例的时候,内部构造函数中的this发生改变指向d1,然后在当前的构造函数内部,再去通过call()去调用构造函数,那么父类中构造函数this指向d1,此方法只能继承父类中的属性,但是方法不能被继承下来。

组合继承

就是将原型链继承与借用构造函数继承的优点结合到一起,然后构成组合继承。

原型链继承的优点:通过重写原型对象,方法会被继承下来。

原型链继承的缺点:父类中的实例属性一旦赋值给子类的原型属性,此时这些属性都属于子类的共享属性,不能在实例化子类构造函数的时候传值。

借用构造函数优点:解决了父类中的实例属性一旦赋值给子类的原型属性,这些属性就会共享的问题。 也解决了不能在实例化子类构造函数的时候传值问题,能传值了。

借用构造函数的缺点,不能把父类中定义的共享的方法继承下来

Dog构造函数的内部还是通过Animal.call()来继承它的属性,继承这个方法的时候去重写一个原型对象

function Animal(name){   //这是定制我们当前的属性 
 //定义一个能共享的名字
 this.name=name;
 this.color=['reg',"green",'blue']
 }
Animal.prototype.getName=function(){
    return this.name;
}
function Dog(name){
    Animal.call(this,name)
}
Dog.prototype=new Animal();
Dog.prototype.constructor=Dog;
var d1 = new Dog("小太阳");
var d2 = new Dog("小胖子");

组合继承模式的优缺点

优点:结合构造函数优点和重写原型对象的优点

让父类的实例属性继承下来,实例修改引用类型的值,然后另一个实例的引用类型的值不会发生变化。(这个是借用构造函数继承的有点)

通过重写原型对象,把父类的共享方法继承下来。(重写原型对象的优点)

缺点: 无论在什么样的情况下,我们要是使用组合继承模式它都会去调用两次父类的构造函数,一个是我们在创建子类原型的时候,一次是初始子类原型对象的时候调用Animal,Dog.prototype=new Animal();这里调用一次。还有一次就是在子类构造函数内部调用父类构造函数的时候,代码执行到 Animal.call(this,name),此处又去调用了当前的函数,又执行了一次,这样完全没必要。

寄生组合式继承(使用最广泛)

寄生组合式继承就是解决组合模式继承带来的问题,组合继承模式的问题是两次调用父类构造函数。

function Animal(name){   //这是定制我们当前的属性 
 //定义一个能共享的名字
 this.name=name;
 this.color=['reg',"green",'blue']
 }
Animal.prototype.getName=function(){
    return this.name;
}
function Dog(name){
    Animal.call(this,name)
}
Dog.prototype=new Animal();
Dog.prototype.constructor=Dog;
var d1 = new Dog("小太阳");
var d2 = new Dog("小胖子");

分析组合模式的问题: 先重写了原型对象再去实例化了,实例化的时候在构造函数内部通过call();间接式调用这个函数的this,那么通过这种方式来做继承,目的是把我们当前父类中的实例属性,现在归为Dog的实例属性,一个实例去修改引用类型的值,另一个实例不会发生修改。所以说代码 Animal.call(this,name)是没有任何问题的。

也就是说第一次调用的时候Dog.prototype=new Animal();没必要再去调用Animal这个函数。

现在通过重写构造函数原型对象,把共享的getName这个方法共享给子类。就是把原型对象上的属性共享给Dog.prototype

function Animal(name){   //这是定制我们当前的属性 
   //定义一个能共享的名字
   this.name=name;
   this.color=['reg',"green",'blue']
   }
  Animal.prototype.getName=function(){
      return this.name;
  }
  function Dog(name){
      Animal.call(this,name)
  }
  Dog.prototype=Object.create(Animal.prototype);
  Dog.prototype.constructor=Dog;
  var d1 = new Dog("小太阳");
  var d2 = new Dog("小胖子");

对象的创建方式中说到字面量创建方式里面有一个方法叫Object.create(对象),传进去的这个对象就作为返回实例对象的一个原型,由此Animal.prototype这个对象上拥有了当前getName这个共享的方法,然后把Animal.prototype作为Dog.prototype上的原型 Dog.prototype=Object.create(Animal.prototype);,把Dog.prototype充当一个实例,把Animal.prototype作为Dog.prototype的原型,那么此时Dog中就拥有Animal原型中的属性和共享的方法、。

**通过这种方法它没有再去调用父类的函数,但是它把父类上的属性都给了子类的原型对象 **

继承总结

原型链继承

特点:重写子类的原型对象,父类原型对象上的属性和方法都会被子类继承。

存在的问题:

  1. 在父类中定义的实例引用类型的属性,一旦被修改,其他的实例也会被修改。
  2. 当实例化子类的时候,不能传递参数到父类

借用构造函数模式

特点:在子类构造函数内部间接调用,父类的构造函数,可以使用call(),apply(),bind()

原理:改变父类中的this指向

优点:仅仅的是把父类中的实例属性当做子类的实例属性,并且还能传参

缺点:父类中共享的方法不能被继承下来

组合继承

特点:结合了原型链和借用构造函数的有点

  1. 原型链继承:共有的方法能被继承下来
  2. 借用构造函数:实例属性能被继承下来

缺点:调用了两次父类构造函数

  1. 实例化子类对象时候调用
  2. 子类构造函数内部调用

寄生式组合模式

通过一个原型对象生成另一个原型对象 将a对象作为实例的原型对象 把子类的原型对象指向了父类的原型对象,通过一个原型对象生成另一个原型对象 var b.prototype = Object.create(a.prototype)

开发过程中使用最多,最广泛的一种继承模式。