微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

继承 原型链

继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承:***接口继承和实现继承。前者只继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签名。实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。

1.原型链

ECMAScript 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性方法。重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。

实现原型链涉及如下代码模式:

// 创建Animal
function Animal() {
  this.name = 'animal';
}
Animal.prototype.getAnimalName = function () {
  console.log(this.name + 'getAnimalName');
}
// 创建Dog
function Dog() {
  this.name = 'dog';
}
// Dog继承自Animal  将Animal的实例赋值给Dog的原型对象,相当于将Animal的实例中的__proto__赋值给了Dog的原型对象
// 如此 Dog原型对象 就能通过 Animal 对象的实例中的[[prototype]](__proto__) 来访问到 Animal原型对象 中的属性方法了。
Dog.prototype = new Animal();
// 不建议使用Dog.prototype.__proto__=== Animal.prototype,因为双下划线的属性是js中的内部属性,各个浏览器兼容性不一,不建议直接操作属性,ES6中提供了操作属性方法可以实现。
console.log(Dog.prototype.__proto__ === Animal.prototype );
// 在使用原型链继承的时候,要在继承之后再去原型对象上定义自己所需的属性方法
Dog.prototype.getDogName = function () {
  console.log(this.name + 'getDogName');
}
var d1 = new Dog();
d1.getAnimalName()
d1.getDogName()

以上代码定义了两个类型:Animal 和 Dog。

这两个类型分别定义了一个属性一个方法。这两个类型的主要区别是通过创建 Animal 的实例并将其赋值给Dog的原型对象,所以Dog. prototype 实现了对 Animal 的继承。这个赋值重写了 Dog 最初的原型,将其替换为Animal 的实例。这意味着 Animal 实例可以访问的所有属性方法也会存在于 Dog. prototype。这样实现继承之后,代码紧接着又给Dog.prototype,也就是这个 Animal 的实例添加一个方法。最后又创建了 Dog 的实例并调用了它继承的 getAnimalName方法

下图展示了子类的实例与两个构造函数及其对应的原型之间的关系。

 

这个案例中实现继承的关键,是 Dog 没有使用认原型,而是将其替换成了一个新的对象。这个新的对象恰好是 Animal 的实例。这样一来,Dog 的实例不仅能从 Animal 的实例中继承属性方法,而且还与 Animal 的原型挂上了钩。于是 d1(通过内部的[[Prototype]] )指向Dog.prototype,而 Dog.prototype(作为 Animal 的实例又通过内部的[[Prototype]])指向 Animal.prototype。注意,getAnimalName()方法还在 Animal.prototype 对象上,而 name 属性则在 Dog.prototype 上。这是因为 getAnimalName()是一个原型方法,而name 是一个实例属性。Dog.prototype 现在是 Animal 的一个实例,因此 name才会存储在它上面。

还要注意,由于 Dog.prototype 的 constructor 属性被重写为指向Animal,所以 d1.constructor 也指向 Animal,想要指回Dog可以修改Dog.prototype.constructor。在读取实例上的属性时,首先会在实例上搜索这个属性。如果没找到,则会继承搜索实例的原型,这就是原型搜索机制。在通过原型链实现继承之后,搜索就可以继承向上,搜索原型的原型。对前面的例子而言,调用 d1.getAnimalName()经过了 3 步搜索:d1、Dog.prototype 和Animal.prototype,最后一步才找

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

1.1.认原型

实际上,原型链中还有一环。认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的。任何函数认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指向Object.prototype。这也是为什么自定义类型能够继承包括 toString()、valueOf()在内的所有方法的原因。因此前面的例子还有额外

一层继承关系。

Dog 继承 Animal,而 Animal 继承 Object。在调用 d1.toString()时,实际上调用的是保存在Object.prototype 上的方法

1.2.原型与继承关系

原型与实例的关系可以通过两种方式来确定。第一种方式是使用 instanceof 操作符,如果一个实例的原型链中出现过相应的构造函数,则 instanceof 返回 true。如下例所示:

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

确定这种关系的第二种方式是使用 isPrototypeOf()方法。原型链中的每个原型都可以调用这个方法,如下例所示,只要原型链中包含这个原型,这个方法就返回 true:

console.log(Object.prototype.isPrototypeOf(d1)); // true 
console.log(Animal.prototype.isPrototypeOf(d1)); // true 
console.log(Dog.prototype.isPrototypeOf(d1)); // true

1.3.关于方法

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

function Animal() {
  this.name = 'animal';
}
Animal.prototype.getAnimalName = function () {
  console.log(this.name + 'getAnimalName');
}
// 创建Animal的实例
var a1 = new Animal()
a1.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('我覆盖了父类方法');
}
var d1 = new Dog();
d1.getAnimalName(); // 我覆盖了父类方法
d1.getDogName();

在上面的代码中。getDogName()方法 是 Dog 的新方法。而最后一个方法 getAnimalName()是原型链上已经存在但在这里被遮蔽的方法。后面在 Dog 实例上调用 getAnimalName()时调用的是这个方法。而 Animal 的实例仍然会调用最初的方法。重点在于上述两个方法都是在把原型赋值为 Animal 的实例之后定义的。

1.4.原型链的破坏

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

function Animal() {
  this.name = 'animal';
}
Animal.prototype.getAnimalName = function () {
  console.log(this.name);
};
function Dog() {
  this.name = 'dog';
}
// 继承
Dog.prototype = new Animal()
Dog.prototype = {
  getDogName() {
    console.log(this.name);
  },
  someOtherMethod() {
    return false;
  }
};
var d1 = new Dog();
d1.getAnimalName(); // 出错!

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

1.5.原型链的问题

原型链虽然是实现继承的强大工具,但它也有问题。主要问题出现在原型中包含引用值的时候。前面在谈到原型的问题时也提到过,原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性摇身一变成为了原型属性

function Animal() {
  this.categorys = ["cat", "rabbit"];
}
function Dog() { }
// 继承 Animal 
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' ]

在这个例子中,Animal 构造函数定义了一个 categorys 属性,其中包含一个数组(引用值)。每个Animal 的实例都会有自己的 categorys 属性,包含自己的数组。但是,当 Dog 通过原型继承Animal 后,Dog.prototype 变成了 Animal 的一个实例,因而也获得了自己的 categorys属性。这类似于创建了Dog.prototype.categorys s属性。最终结果是,Dog 的所有实例都会共享这个 categorys 属性。这一点通过d1.categorys 上的修改也能反映到 d2.categorys上就可以看出来。

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

原文地址:https://www.jb51.cc/wenti/3281759.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐