JS(十一)原型链继承、经典继承、组合继承

1.原型链继承

构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。

什么是原型?

每一个函数都有一个属性叫做prototype,这个值是一个对象,默认只有一个叫做constructor的属性,指向这个函数对象

 原型就是一个对象,其他的对象可以通过它实现一个属性的继承。

详细理解原型:

①所有引用类型都有一个__proto__(隐式原型)属性,属性值是一个普通的对象

②所有函数都有一个prototype(原型)属性,属性值是一个普通的对象

③所有引用类型的__proto__属性指向它构造函数的prototype

什么是原型链?

当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。

var obj = new Object();

console.log(Object.prototype === obj.__proto__);//true

console.log(Object.prototype ===obj.prototype)//false

原型特点:

  • JavaScript对象是通过引用来传递的,当修改原型时,与之相关的对象也会继承这一改变

原型链继承(将父类的实例作为子类的原型):

//父类
function Animal(){
  this.name ='animal';
}
Animal.prototype.getAnimalName=function(){
  console.log(this.name)
}
//子类
function Dog(){
  this.name='dog';
}
// Dog继承与Animal
Dog.prototype= new Animal();
var d1=new Dog();
d1.getAnimalName();//dog
// 在使用原型链继承的时候,要在继承之后再去原型对象上定义自己所需的属性和方法
Dog.prototype.getDogName = function () {
  console.log(this.name +'-'+ 'getDogName');
}
d1.getDogName();//dog-getDogName

//instanceof运算符用于检测构造函数的 `prototype` 属性是否出现在某个实例对象的原型链上。
console.log(d1 instanceof Object);  //true
console.log(d1 instanceof Animal);  //true
console.log(d1 instanceof Dog);     //true

1.1 关于方法

子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,这些方法必须在原型赋值之后再添加到原型上。来看下面的例子:

//父类
function Animal(){
  this.name ='animal';
}
Animal.prototype.getAnimalName=function(){
  console.log(this.name)
}
//子类
function Dog(){
  this.name='dog';
}
// Dog继承与Animal
Dog.prototype= new Animal();
Dog.prototype.getDogName = function () {
  console.log(this.name );
}
// 在子类设置同名方法 覆盖父类的方法
Dog.prototype.getAnimalName=function(){
  console.log('我覆盖了父类方法')
}
var d1 = new Dog();
d1.getAnimalName(); // 我覆盖了父类的方法
var a1=new Animal();
a1.getAnimalName()//animal

上面代码在子类设置同名方法 覆盖父类的方法,设置属性也是同样道理可以覆盖(原型链的原理)

1.2原型链的破坏

以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。

function Animal() {
  this.name = 'animal';
}
Animal.prototype.getAnimalName = function () {
  console.log(this.name);
};
function Dog() {
  this.name = 'dog';
}
// 继承
Dog.prototype = new Animal()
 //重新指向了Object,不是Animal实例了,上面的继承不起作用
Dog.prototype = {
  getDogName() {
    console.log(this.name);
  },
  someOtherMethod() {
    return false;
  }
};
var d1 = new Dog();
d1.getAnimalName(); // 出错!

在这段代码中,子类的原型在被赋值为 Animal 的实例后,又被一个对象字面量覆盖了。覆盖后的原型是一个 Object 的实例,而不再是 Animal 的实例。因此之前的原型链就断了。Dog和 Animal 之间也没有关系了。

1.3原型链的问题

1.所有实例都会共享这个 categorys 属性。导致改变某一个的属性值,其他实例的同名属性的值也会发生改变问题。

 2.子类型在实例化时不能给父类型的构造函数传参。再加上之前提到的原型中包含引用值的问题,就导致原型链基本不会被单独使用。

//父类
function Animal(){
  this.name ='animal';
  this.categorys = ['cat','rabbit'];
}
//子类
function Dog(){
  this.name='dog';
}
Dog.prototype=new Animal();
var d1 = new Dog();
d1.categorys.push('dog')
console.log(d1.categorys);//[ 'cat', 'rabbit', 'dog' ]
var d2 = new Dog();
console.log(d2.categorys);//[ 'cat', 'rabbit', 'dog' ]
console.log(d2);//Animal { name: 'dog' }

 通过d1.categorys 上的修改也能反映到 d2.categorys上就可以看出来问题1了。

原型链的第二个问题是,子类型在实例化时不能给父类型的构造函数传参。事实上,我们无法在不影响所有对象实例的情况下把参数传进父类的构造函数。再加上之前提到的原型中包含引用值的问题,就导致原型链基本不会被单独使用。

2.经典继承

为了解决原型包含引用值导致的继承问题,一种叫作“盗用构造函数”(constructor stealing)的技术在开发社区流行起来(这种技术有时也称作“对象伪装”或“经典继承”)。 基本思路很简单:在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()和 call()方法以新创建的对象为上下文执行构造函数。

//父类
function Animal(name){
  this.name=name;
  this.category = ['cat','dog'];
  sayName=function(){
    console.log(this.name)
  }

}
// 子类
function Dog(age){
  //使用call或apply方法改变构造函数this的指向 
  //如果创建每次不同的构造函数相当于每次自己创建了属于自己的属性和方法,浪费内存  创建的实例并不是父类的实例,只是子类的实例。
  Animal.call(this,'zhangsan');//this指向d1
  this.age=age;
  
}
// function Dog(age){
//  // this.age=age;//this指向d1
  
// }
var d1 = new Dog(19);
var d2 = new Dog(219);
d1.category.push='qwq'
console.log(d1)//Dog {name: 'zhangsan',category: [ 'cat', 'dog', push: 'qwq' ],age: 19}
console.log(d2)//Dog { name: 'zhangsan', category: [ 'cat', 'dog' ], age: 219 }
console.log(d2.sayName)//undefined

解决了原型继承共享及传参的问题。相比于使用原型链,经典继承函数的一个优点就是可以在子类构造函数中向父类构造函数传参。

缺点:必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。由于存在这些问题,经典继承函数基本上也不能单独使用。

总结:

1.创建的实例并不是父类的实例,只是子类的实例。

2.没有拼接原型链,不能使用instanceof。因为子类的实例只继承了父类的实例属性/方法,没有继承父类的构造函数的原型对象中的属性/方法。

3.每个子类的实例都持有父类的实例方法的副本,浪费内存,影响性能,而且无法实现父类的实例方法的复用。

3.组合继承

组合继承(有时候也叫伪经典继承)综合了原型链和经典继承函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过经典继承函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

function Animal(name) {
  this.name = name;
  this.categorys = ["cat", "rabbit"];
}
Animal.prototype.sayName = function () {
  console.log(this.name);
};
function Dog(name, age) {
  // 继承属性
  Animal.call(this, name);
  this.age = age;
}
// 继承方法
Dog.prototype = new Animal();
Dog.prototype.sayAge = function () {
  console.log(this.age);
};
var d1 = new Dog("zhangsan", 29);
d1.categorys.push("dog");
console.log(d1.categorys); // [ 'cat', 'rabbit', 'dog' ]
d1.sayName(); // zhangsan
d1.sayAge(); // 29 
var d2 = new Dog("lisi", 27);
console.log(d2.categorys); // [ 'cat', 'rabbit' ]
d2.sayName(); // lisi
d2.sayAge(); // 27

 组合继承弥补了原型链和经典继承函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值