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__
,在这个时候我们改变了Dog
的prototype
。这个原型对象,改变成new Animal();
。
new Animal();
也是一个实例对象,假设我们以new Animal();
来一个实例var a= new Animal();
,然后我们实例出来以后会有它自己的__proto__
,那么它的这个实例指向了Animal
的原型,那么这个时候Animal
原型上的constructor
、getName
都归当前实例出来的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 a
的constructor
也是指向了当前的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 d1
的constructor
指向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;
现在d1
和d2
都会共有这个数组。
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();
中name
和color
作为一个实例属性存在,但是现在一旦赋值给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();
间接式调用这个函数的thi
s,那么通过这种方式来做继承,目的是把我们当前父类中的实例属性,现在归为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原型中的属性和共享的方法、。
**通过这种方法它没有再去调用父类的函数,但是它把父类上的属性都给了子类的原型对象 **
继承总结
原型链继承
特点:重写子类的原型对象,父类原型对象上的属性和方法都会被子类继承。
存在的问题:
- 在父类中定义的实例引用类型的属性,一旦被修改,其他的实例也会被修改。
- 当实例化子类的时候,不能传递参数到父类
借用构造函数模式
特点:在子类构造函数内部间接调用,父类的构造函数,可以使用call(),apply(),bind(),
原理:改变父类中的this指向
优点:仅仅的是把父类中的实例属性当做子类的实例属性,并且还能传参
缺点:父类中共享的方法不能被继承下来
组合继承
特点:结合了原型链和借用构造函数的有点
- 原型链继承:共有的方法能被继承下来
- 借用构造函数:实例属性能被继承下来
缺点:调用了两次父类构造函数
- 实例化子类对象时候调用
- 子类构造函数内部调用
寄生式组合模式
通过一个原型对象生成另一个原型对象
将a对象作为实例的原型对象
把子类的原型对象指向了父类的原型对象,通过一个原型对象生成另一个原型对象
var b.prototype = Object.create(a.prototype)
开发过程中使用最多,最广泛的一种继承模式。