【JavaScript】-- 三种继承方式

本文详细介绍了JavaScript中的原型链概念,包括构造函数、原型和实例的关系,以及如何通过原型链实现继承。通过示例展示了Animal和Dog两个类型的继承关系,解释了`instanceof`和`isPrototypeOf()`运算符的用法。此外,还讨论了原型链的破坏、问题以及经典的构造函数继承。最后,提出了组合继承作为解决这些问题的常见模式,它结合了原型链和构造函数继承的优点。
摘要由CSDN通过智能技术生成

注意:在JS里面是没有子类和父类的概念的,在这里只是为了方便才简称为子类与父类。我们应该完整的将其描述为子类构造函数与父类构造函数。

原型链

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

如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。

实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。

 原型链实现代码如下:

// 创建Animal
function Animal(){
    this.name = 'Animal';
}
// prototype
Animal.prototype.getAnimalName = function(){
    console.log(this.name + 'getAnimalName');
}
// 创建Dog
function Dog(){
    this.name = "dog";
}
// 这一句将Animal继承给Dog
// 将Animal的实例赋值给Dog的prototype对象,相对于将Animal的实例中的__proto__赋值给Dog的prototype对象
// 如此一来,就能通过Animal的[[prototype]](__proto__)来访问到Dog的原型对象中的属性与方法
Dog.prototype = new Animal()
// 不建议使用Animal.__proto__ === Dog.prototype
// 在使用原型链继承时,要先继承再在自己的原型对象里定义自己的属性和方法
Dog.prototype.getDogName = function(){
    console.log(this.name + 'getDogName');
}
var dog1 = new Dog();
dog1.getAnimalName();
dog1.getDogName();

注意:getAnimalName() 是一个方法,在Animal.protptype对象上,而name是一个属性在Dog.prototype上,因为getAnimalName是一个原型方法,name是一个实例属性

在这里我们是定义了两个类型Animal与Dog

并将Animal的实例赋值给Dog,于是Dog.prototype实现了对Animal的继承

这样赋值重写了Dog最初的原型对象,使其变成了Animal的实例

这样写使得Animal可以访问的属性和方法都存在于Dog.prototype

在这样实现了继承之后我们又向 Dog.prototype 里面添加了一个 getDogName 的方法

最后创建Dog的实例 dog1 并调用其继承的 getAnimalName 方法

dog1通过内部的[[prototype]]属性指向Dog.prototype,

Dog又通过内部的[[prototype]]属性指向Animal.prototype

由于将Dog.prototype的constructor属性指向了Animal,因此dog1的constructor也指向了Animal

可以通过修改 Dog.prototype.constructor 来改变指向,使其重新指回Dog

当我们想要读取实例上的属性时,会首先在实例上进行搜索,

如果没有找到,就往上搜索实例的原型

如果还没有找到就继续往上搜索原型的原型(前提是完成了原型链的继承)

dog1.getAnimalName()总共进行了三步搜索:dog1--Dog.prototype--Animal.prototype

对属性和方法的搜索会一直持续到原型链的末端

默认原型

在原型链中还有一环,就是在默认情况下所有的引用类型都继承自Object

任何函数的默认原型都是Object的一个实例,意味着实例内部还有一个内部指针指向Object.prototype

这也就是为什么自定义类型能够继承包括 toString()、valueOf()在内的所有默认方法的原因。

原型与继承关系

在上面的案例中,我们可以用instanceof运算符用于检测构造函数的protptype属性是否在某个实例的原型链上:

// instanceof运算符用于检测构造函数的protptype属性是否在某个实例的原型链上
console.log(dog1 instanceof Object);  // true
console.log(dog1 instanceof Animal);  // true
console.log(dog1 instanceof Dog);  // true
// isPrototypeOf() 原型链中每个原型都可以调用这个方法,若原型链中包含这个原型,返回true
console.log(Object.prototype.isPrototypeOf(dog1));
console.log(Animal.prototype.isPrototypeOf(dog1));
console.log(Dog.prototype.isPrototypeOf(dog1));

让子类上的方法覆盖父类上的方法,或者在子类里添加新的方法: 

// 若要让子类上的方法覆盖父类上的方法,或者在子类添加新方法,则需要先给在原型赋值之后再添加到原型上
function Animal(){
    this.name = "animal";
}
Animal.prototype.getAnimalName = function(){
    console.log(this.name + "getAnimalName");
}
var test = new Animal();
test.getAnimalName();  // animalgetAnimalName
function Dog(){
    this.name = "dog";
}
Dog.prototype = new Animal();
Dog.prototype.getDogName = function(){
    console.log(this.name + "getDogName");
}

Dog.prototype.getAnimalName = function(){
    console.log(this.name + "我被覆盖了");
}
var dog2 = new Dog();
dog2.getAnimalName();  // dog我被覆盖了
dog2.getDogName();  // doggetDogName

getDogName()方法是 Dog 的新方法

而后一个 getAnimalName()是原型链上已经存在但在这里被遮蔽的方法

后面在Dog实例上用的就是后面这个被改写过的方法

而第一次Animal的实例执行的仍然是最初的原型里的方法

重点在于上述两个方法都是在把原型赋值为 Animal 的实例之后定义的。

 原型链的破坏:

以对象字面量形式创建原型方法就可以破坏原型链:

// 只要以对象字面量的形式创建原型方法就可以破坏原型链,因为这相当于重写了原型链
function Animal() {
  this.name = "animal";
}
Animal.prototype.getAnimalName = function () {
  console.log(this.name + "getAnimalName");
};
function Dog(){
    this.name = "dog";
}
// 继承
Dog.prototype = new Animal();
// 以对象字面量形式创建原型方法
Dog.prototype = {
    getDogName(){
    console.log(this.name + "getDogName");
    },
    someOtherMethod() {
        return false;
    }
};
var dog = new Dog();
dog.getDogName();  // doggetDogName
dog.getAnimalName();  // 报错!

在这里,Dog被赋值为一个Animal的实例后,又被一个对象字面量覆盖了

覆盖后的原型是一个Object的·实例,而不是Animal的实例

因此原先的原型链就断了,Dog和Animal也就没了联系

原型链的问题:

  •  类似于原型模式的问题, 原型中包含的引用值会在所有实例间共享
function Animal(){
  this.category = ['cat', 'rabbit']
}
function Dog(){};
// 继承
Dog.prototype = new Animal();
var dog1 = new Dog();
dog1.category.push('piggy');
console.log(dog1.category);  // [ 'cat', 'rabbit', 'piggy' ]
var dog2 = new Dog();
console.log(dog2.category);  // [ 'cat', 'rabbit', 'piggy' ]

在这里我们在Animal构造函数里面定义了一个category属性,并传入一个数组(引用值)

因此每个Animal的实例里都会有一个category属性,包括其中的数组

当我们在后面完成了Dog原型对Animal的继承时,Dog.prototype也成为了一个Animal的实例,获得了category属性

也就相当于创建了一个Dog.prototype.category属性

结果Dog的每个实例都会共享这个category属性,因此我们在修改dog1.category时也会修改dog2.category

 原型链的第二个问题是:

  • 无法在不影响所有对象实例的情况下将参数传入父类的构造函数里

盗用构造函数/对象伪装/经典继承

这是为了解决原型的包含引用值的问题。

思路:在子类构造函数中调用父类构造函数。

因为函数就是在特定上下文执行代码的简单对象,因此我们可以使用call或者apply方法以新创建的对象为上下文执行代码

function Animal(){
  this.category = ['cat', 'rabbit']
}
function Dog(){
  // 继承
  Animal.call(this)
};
var dog1 = new Dog();
dog1.category.push('piggy');
console.log(dog1.category);  // [ 'cat', 'rabbit', 'piggy' ]
var dog2 = new Dog();
console.log(dog2.category);  // [ 'cat', 'rabbit' ]

var dog1 = new Dog();是dog1调用Dog构造函数,因此其内部的this指向dog1,

Animal.call(this)相当于是Animal.call(dog1),即dog1.Animal()

当dog1调用Animal里的方法时,Animal内部的this就指向了dog1

因此Animal上所有的属性和方法都被拷贝到了dog1上面

所以每个实例都会有自己的category属性副本,互不影响

  经典继承还可以向父类构造函数内传递参数:

// 经典继承还可以向父类构造函数内传递参数
function Animal(name){
  this.name = name;
}
function Dog(){
  Animal.call(this,'wangwang');
  this.age = 3;
}
var dog = new Dog();
console.log(dog.name);  // wangwang
console.log(dog.age);  // 3

在这里我们向Animal里传递了一个参数name,然后将它赋值给一个属性

在Dog构造函数调用Animal构造函数时同时将这个参数传入,实际上也会在Dog构造函数里定义一个name属性

为了不让父类构造函数将子类构造函数上的属性覆盖,我们可以先调用父类构造函数再给子类构造函数添加别的属性

经典继承的问题

经典继承的问题也就是构造函数模式的问题,即必须在构造函数中定义方法,函数不能复用,

子类也不能访问父类原型上定义的方法,所以经典继承也不能单独使用。

总结:

  1. 创建的实例只是子类的实例,不是父类的实例
  2. 没有拼接原型链,不能使用instanceof(),因为子类只继承了父类里的属性和方法,没有继承父类原型对象里的属性和方法
  3. 每个子类实例都保存了父类的实例方法副本,浪费内存影响性能,而且不能实现父类实例方法的复用

组合继承(伪经典继承)

结合了原型链和经典继承

思路:使用原型链继承原型上的属性和方法,使用经典继承函数继承实例属性。

这样做既可以将方法定义在原型上以实现复用,又可以使每个实例拥有自己的属性

 组合继承代码如下:

function Animal(name){
  this.name = name;
  this.category = ['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 dog = new Dog("love", 11);
dog.category.push('pig');
console.log(dog.category);
dog.sayName();
dog.sayAge();
var doggy = new Dog("sick", 9);
console.log(doggy.category);
doggy.sayName();
doggy.sayAge();

在这个例子里,我们在Animal的构造函数里定义了两个属性name和category

在Animal的原型对象里定义了方法sayName()

Animal.call(this,name);使Dog构造函数调用了Animal构造函数,并传入name属性

然后又额外定义了一个age属性

然后,Dog.prototype 也被赋值成为Animal的实例,原型实例被赋值后又添加了一个叫sayAge()的方法

这样,我们就可以创建两个Dog实例,让这两个实例都有自己的属性,同时共享相同的方法

组合继承弥补了原型链和经典继承函数的不足,是 JavaScript 中使用最多的继承模式。

而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值