欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JS高程第六章笔记(下)- 继承

程序员文章站 2022-03-04 13:23:21
...

JavaScript 高级程序设计-第六章-面向对象的程序设计-继承

继承

继承是面向对象语言的基本特征之一,继承有接口继承和实现继承,由于 js 函数没有签名(Java 的方法签名是方法名+形参列表组成的),所以 js 只支持实现继承,实现继承主要依靠原型链来实现。

原型链

回忆原型模式,如果让一个原型对象 A 等于另一个类型的实例 B 呢,那这个原型对象 A 就包含一个指针指向实例 B 的原型对象。如此层层递进,例子:

function SuperType() {
    this.superValue = true
}
SuperType.prototype.getSuperValue = function () {
    return this.superValue
}
function SubType() {
    this.subValue = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
    return this.subValue
}
var instance = new SubType()
console.log(instance.getSubValue())

例子中 SubType 继承了 SuperType,用 SuperType 的实例重写了 SubType 的原型对象。因为实例是可以指向它的原型对象的,因此,例子中的是实例 instance 指向 SubType 的原型(也就是 SuperType 的实例),SubType 的原型(也就是 SuperType 的实例)又指向了 SuperType 的原型。

getSuperValue 方法在 SuperType 的原型中;superValue 在 SubType 的原型中(因为 superValue 是实例属性,而 SuperType 的实例重写了 SubType 的原型)。调用 instance.getSubValue() 的过程是:1.搜索实例;2.搜索 SubType.prototype;3.搜索 SuperType.prototype。

别忘记默认的原型链

所有的引用类型都默认继承了 Object,也是通过原型链继承的。像 toString() 这种方法就是调用的 Object.prototype 上的方法。

确定原型和实例的关系

两种方式确认:

  1. instanceof 操作符:
alert(instance instanceof Object)       //true
alert(instance instanceof SuperType)    //true
alert(instance instanceof SubType)      //true

由于原型链,instance 都可以看作是 Object,SuperType,SubType 三者的实例。

  1. isPrototypeOf() 方法:
alert(Object.prototype.isPrototypeOf(instance))     //true
alert(SuperType.prototype.isPrototypeOf(instance))  //true
alert(SubType.prototype.isPrototypeOf(instance))    //true

谨慎地定义方法

  1. 给原型添加方法需要在替换原型之后。
  2. 通过原型链实现继承时,不能使用对象字面量创建原型方法。

原型链的问题

  1. 上文中的组合使用构造函数和原型来生成新的类型的方法就是为了解决是引用类型的原型属性会被所有的实例共享的问题。原型链继承同样会有这个问题,例子:
function SuperType() {
    this.colors = ['red', 'blue', 'green']
}
function SubType() {}
// SubType 继承 SuperType
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors.push('black')
alert(instance1.colors) // "red,blue,green,black"
var instance2 = new SubType()
alert(instance2.colors) // "red,blue,green,black"

colors 数组是定义在构造函数中的,因此所有的 SuperType 实例都会有自己的 colors 数组。因为 SubType 的原型是 SuperType 的实例,因此 SubType 的原型也有一个 colors 数组,但是这就是导致了上文中讨论的问题,原型中的引用类型会被所有的实例共享,在例子中也就是被所有的 SubType 实例共享。

  1. 创建子类型时不能向超类型的构造函数传递参数。

借用构造函数模式

为了解决原型中包含引用类型带来的继承问题,采用 constructor stealing 的技术。在子类型的构造函数内部调用超类型的构造函数,例子:

function SuperType(){
    this.colors = ["red", "blue", "green"]
function SubType(){
    // 继承 SuperType,apply() 方法也可以
    SuperType.call(this)
}
var instance1 = new SubType()
instance1.colors.push("black")
alert(instance1.colors)    //"red,blue,green,black"
var instance2 = new SubType()
alert(instance2.colors)    //"red,blue,green"

这样每一个 SubType 实例都有自己的 colors 副本了。

传递参数

借用构造函数还可以往构造函数中传递参数,例子:

function SuperType(name){
    this.name = name
}
function SubType(){
    //继承了 SuperType,同时还传递了参数 SuperType.call(this, "Nicholas");
    //实例属性
    this.age = 29;
}
var instance = new SubType()
alert(instance.name)    //"Nicholas";
alert(instance.age)     //29

借用构造函数的问题

借用构造函数模式也有问题,还是构造函数模式的老问题,没有复用性。而且超类中的方法在子类中不可见。

所以构造函数模式也很少用。

*组合继承

组合继承,也叫伪经典继承。是用原型链和借用构造函数模式组合到一起使用。思路是使用原型链实现对原型属性和方法的继承,用借用构造函数来实现对实例属性的继承,满足了复用性和每个实例都有自己的属性,例子:

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

在例子中:

  1. 超类的构造函数定义了两个属性,超类的原型对象定义了一个方法。
  2. 子类的构造函数借用了超类的构造函数并传入了参数,定义了自己的属性。
  3. 用超类的实例重写子类的原型对象,在子类新原型上指定 constructor 属性为子类的构造对象,并在子类新原型上添加了自己的方法。

这样使得不同的实例有自己的属性,使用相同的方法。这是 JavaScript 中最常见的继承模式。

原型式继承

道格拉斯·克罗克福德于2006年提出的这种继承模式。借助原型基于已有的对象创建新的对象,不创建新的类型。

function object(o) {
    // 临时的构造函数
    function F() {}
    // 传入的对象重写临时构造函数的原型
    F.prototype = o
    // 返回这个新临时类型的一个实例
    return new F()
}

使用例子:

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

原型式继承,必须有一个对象作为另一个对象的基础,基于 object 函数返回的对象根据自己的需要进行修改。但很明显引用属性会被公用。

ES5 通过新增的 Object.create() 方法规范了原型式继承,create 方法接收两个参数。一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象,(一个参数)只有作为原型的对象参数的例子:

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

create 方法的第二个参数与 Object.defineProperties() 方法的第二个参数格式相同,第二个参数中指定的属性会覆盖原型对象上的同名属性,两个参数的例子:

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object.create(person,{
    name: {
        value: "Greg"
    }
});
alert(anotherPerson.name);  // "Greg"

在只是想让一个对象与另一个对象类似的情况下可以采用原型式继承,但是缺点是引用属性会被公用。

寄生式继承

寄生式继承也是道格拉斯·克罗克福德提出的,创建一个仅用于封装继承过程的函数,在函数内部增强对象,例子:

function createAnother(original) {
    var clone = object(original)
    clone.sayHi = function () {
        alert('Hi!')
    }
    return clone
}

createAnother 函数接收了一个参数作为新对象的基础,将这个参数传递给了上文中的 object 函数。将返回的值赋给 clone,新对象添加新方法,最后返回新对象,使用例子:

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

寄生式继承的缺点是函数不能复用。

*寄生组合式继承

组合继承是最常用的模式,但是也有缺点,会调用两次超类的构造函数:

  • 一次用超类的实例(调用超类的构造函数)重写子类的原型时。
  • 一次是子类型的构造函数内部借用构造函数(借用超类的构造函数)时。

复习组合继承的例子:

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name, age){
    // 第二次调用SuperType(),一组 name,colors 属性在子类实例上
    SuperType.call(this, name);
    this.age = age;
}
// 第一次调用SuperType(),,一组 name,colors 属性在子类原型上
// 用超类的实例来重写子类的原型
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};

两次调用超类的构造函数,子类都会继承超类的所有实例属性,第二次调用会用新的继承自超类实例属性屏蔽第一次调用时继承的。

通过寄生组合式继承来解决这个问题,通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。思路是用寄生式继承来代替“用超类的实例来重写子类的原型”的过程,例子:

function inheritPrototype(subType, superType) {
    var prototype = object(superType.prototype) // 超类原型的副本
    prototype.constructor = subType  // 超类原型副本的 constructor 属性指定为子类的构造函数
    subType.prototype = prototype   // 用超类原型副本重写子类的原型
}

用这个 inheritPrototype 函数来代替上文例子中的“用超类的实例来重写子类的原型”过程(叫寄生组合式继承模式):

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

寄生组合式继承模式高效率在于只调用了一次 SuperType 构造函数,避免创建不必要的属性,同时保持原型链不变,还能正常使用 instanceof 操作符和 isPrototypeOf() 方法。被认为是引用类型最理想的继承范式。

相关标签: ECMAScript