JavaScript高级教程-继承

一、继承

1. 原型链

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

  如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性摇身一变成为了原型属性。

function SuperType() { 
	this.property = true;
}
SuperType.prototype.getSuperValue = function() { 
	return this.property; 
};
function SubType() { 
	this.subproperty = false; 
}
SubType.prototype = new SuperType();       // 继承 SuperType
SubType.prototype.getSubValue = function () { 
	return this.subproperty; 
};
let instance = new SubType();
console.log(instance.getSuperValue());        // true

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

原型链图示
在这里插入图片描述

(1) 默认原型

  默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的。任何函数的默认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指向Object.prototype。

完整原型链图示
在这里插入图片描述

(2) 原型与继承关系

原型与实例的关系可以通过两种方式来确定。第一种方式是使用 instanceof 操作符:

console.log(instance instanceof Object);         // true
console.log(instance instanceof SuperType);   // true
console.log(instance instanceof SubType);      // true

第二种方式是使用 isPrototypeOf()方法:

console.log(Object.prototype.isPrototypeOf(instance));         // true
console.log(SuperType.prototype.isPrototypeOf(instance));   // true
console.log(SubType.prototype.isPrototypeOf(instance));      // true

(3) 关于方法

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

(4) 原型链的问题

主要问题出现在原型中包含引用值的时候。

原型链的第二个问题是,子类型在实例化时不能给父类型的构造函数传参。

2. 解决原型包含引用值导致的继承问题

(1) 盗用构造函数

基本思路很简单:在子类构造函数中调用父类构造函数。

function SuperType() {
    this.colors = ["red", "blue", "green"];
}
function SubType() {
     SuperType.call(this);                  // 继承 SuperType
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);        // "red,blue,green,black"
let instance2 = new SubType();      //每次调用每次重新继承
console.log(instance2.colors);        // "red,blue,green"

  通过使用 call()(或 apply())方法, SuperType构造函数在为 SubType 的实例创建的新对象的上下文中执行了。这相当于新的 SubType 对象上运行了SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的 colors 属性。

  相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。

function SuperType(name){
     this.name = name;
}
function SubType() {
     // 继承 SuperType 并传参
     SuperType.call(this, "Nicholas");
     // 实例属性
     this.age = 29;
}
let instance = new SubType();
console.log(instance.name);       // "Nicholas";
console.log(instance.age);          // 29

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

(2) 组合继承

基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。

function SuperType(name){
     this.name = name;
     this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() { console.log(this.name); };
function SubType(name, age){
     // 继承属性
     SuperType.call(this, name);
     this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() { console.log(this.age); };
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors);                 // "red,blue,green,black"
instance1.sayName();                              // "Nicholas"
instance1.sayAge();                                 // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors);                 // "red,blue,green"
instance2.sayName();                              // "Greg";
instance2.sayAge();                                 // 27

  SuperType 构造函数定义了两个属性, name 和 colors,而它的原型上也定义了一个方法叫 sayName()。 SubType 构造函数调用了 SuperType 构造函数,传入了 name 参数,然后又定义了自己的属性 age。此外, SubType.prototype 也被赋值为 SuperType 的实例。原型赋值之后,又在这个原型上添加了新方法 sayAge()。这样,就可以创建两个 SubType 实例,让这两个实例都有自己的属性,包括 colors,同时还共享相同的方法。

3. 原型式继承

  即使不自定义类型也可以通过原型实现对象之间的信息共享。用于这种情况:你有一个对象,想在它的基础上再创建一个新对象。

function object(o) {
     function F() {}
     F.prototype = o;
     return new F();
}
let person = {
      name: "Nicholas",
      friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends);       // "Shelby,Court,Van,Rob,Barbie"

  person 对象定义了另一个对象也应该共享的信息,把它传给 object()之后会返回一个新对象。这个新对象的原型是 person,意味着它的原型上既有原始值属性又有引用值属性。这也意味着 person.friends 不仅是person 的属性,也会跟anotherPerson 和 yetAnotherPerson 共享。

  Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。

let person = {
     name: "Nicholas",
     friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends);        // "Shelby,Court,Van,Rob,Barbie"

  Object.create()的第二个参数与 Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述。

let person = {
     name: "Nicholas",
     friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person, {
     name: {
           value: "Greg"
     }
});
console.log(anotherPerson.name); // "Greg"

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。

4. 寄生式继承

创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

function createAnother(original){
     let clone = object(original);    // 通过调用函数创建一个新对象
     clone.sayHi = function() {       // 以某种方式增强这个对象
           console.log("hi");
     };
     return clone;                            // 返回这个对象
}

  createAnother()函数接收一个参数,就是新对象的基准对象。这个对象 original会被传给 object()函数,然后将返回的新对象赋值给 clone。接着给 clone 对象添加一个新方法sayHi()。最后返回这个对象。

let person = {
     name: "Nicholas",
     friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi();                   // "hi"

  这个例子基于 person 对象返回了一个新对象。新返回的 anotherPerson 对象具有person 的所有属性和方法,还有一个新方法叫 sayHi()。寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。
  通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。

5. 寄生式组合继承

  组合继承存在效率问题,一次在是创建子类原型时调用父类构造函数,另一次是在子类构造函数中调用父类构造函数。

function SuperType(name) {
     this.name = name;
     this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() { console.log(this.name); };
function SubType(name, age){
     SuperType.call(this, name);                       // 第二次调用 SuperType()
     this.age = age;
}
SubType.prototype = new SuperType();         // 第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {  console.log(this.age);  };

  寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

function inheritPrototype(subType, superType) {
     let prototype = object(superType.prototype);   // 创建对象复制副本
     prototype.constructor = subType;                     // 增强对象
     subType.prototype = prototype;                        // 赋值对象
}

  这个 inheritPrototype()函数实现了寄生式组合继承的核心逻辑。这个函数接收两个参数:子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的prototype 对象设置 constructor 属性,解决由于重写原型导致默认 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。如下例所示,调用 inheritPrototype()就可以实现前面例子中的子类型原型赋值:

function SuperType(name) {
     this.name = name;
     this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {  console.log(this.name);  };
function SubType(name, age) {
     SuperType.call(this, name);
     this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {  console.log(this.age);  };

  这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性,因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此 instanceof 操作符和isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值