JavaScript学习(3) – 聊聊原型链- 3. 原型链与继承

《JavaScript高级程序设计(第三版)》学习笔记

Github笔记链接(持续更新中,欢迎star,转载请标注来源)

笔记目录:

JavaScript学习(1) – JavaScript历史回顾

JavaScript学习(2) – 基础语法知识

JavaScript学习(3)- 聊聊原型链- 1. 变量

JavaScript学习(3)- 聊聊原型链- 2. 对象与原型

JavaScript学习(3)- 聊聊原型链- 3. 原型链与继承


关键词:实现继承、原型链的原理与本质、原型继承、组合继承、寄生继承、寄生组合式继承

本章节学习路线:

  1. 通过原型链实现继承
  2. 原型链本身存在的问题:同原型对象一样,引用类型作为共享属性会导致互相干扰
  3. 解决原型链问题的方法:借用构造函数

1. 实现继承

1.1 JavaScript只支持实现继承

由于JavaScript中不具备类似Java中的interface这种方法,所以其继承的方法仅提供实现继承,具体是通过原型链来实现的

1.2 补充知识:实现继承与引用继承(Java说明)

  1. 实现继承:继承实际的方法
  2. 引用继承:继承方法和签名

为更清楚的解释这两个概念,下面通过Java来具体说明:

1.2.1 实现继承:继承实际的方法

// 1. 实现继承 - 继承实际的方法
// 定义一个bird类,其中包含公共方法fly()
public class bird {
  public void fly()
}

// 通过extends使eagle类继承实现bird类,并重写fly()
// 注意:extends后只能继承一个类
public class eagle extends bird {
  @override
  public void fly()
}

复制代码

1.2.2 引用继承:继承方法和签名

// 2. 引用继承 - 继承方法和签名
// 定义接口1
public interface ServeBall {
	public void ServeBalls();
}

// 定义接口2
public interface GiveBirth {
	public void HaveBaby();
}

// 接口继承方法1:通过implements使新建的类实现上述两个接口,并对其中方法进行重写(继承多个接口)
public class WomenBasketballPlayer implements ServeBall,GiveBirth {
	@Override
	public void HaveBaby() {
		// TODO Auto-generated method stub
	}

	@Override
	public void ServeBalls() {
		// TODO Auto-generated method stub
	}
}

// 接口继承方法2:接口继承接口:
public interface GiveBirth extends ServeBall{
	public void HaveBaby();
}
复制代码

1.2.3 额外补充知识:Java中的方法签名

Definition:

Two of the components of a method declaration comprise the method signature—the method’s name and the parameter types.

  1. 方法签名的组成:方法名称+参数类型;例如:calculate(value1, value2)

  2. 意义:用于重载 – 方法签名是可以唯一的确定一个method,与method的返回值一点关系都没有,这是判断重载重要依据

  // 不允许的写法:方法签名完全相同,无法区分进行重载
  public long calculate() {}
  public int calculate() {}
  
  // 允许的写法:方法签名不同
  public long calculate(value1, value2, value3) {}
  public long calculate2(value1, value2, value3)
  public long calculate2(value1, value2) {}
复制代码
  1. 备注:如前文(基本概念)中提过,javascript中并不支持重载,因为后定义的函数会覆盖之前名称相同的函数

  2. 参考文档:

1.3 JavaScript不支持重载

后定义的函数会覆盖之前名称相同的函数。而并不能有java的重载的效果

function foo(num) {
  return num + 100
}

function foo(num) {
  return num + 200
}

var result = foo(100) //300
复制代码

2. 原型链

2.1 基本概念

  1. 原型链的本质:将父类属性继承到子类的prototype中,然后层层嵌套继承
  • 一个对象,继承父类的属性时,会把父类的属性(包括实例属性和原型属性)放在自己的prototype中;
  • 父类继承上一层(例如:Object)的属性时,会把上一层的属性放在父类自己的prototype中;
  • 子类可以通过层层嵌套的原型链,向上溯源,直接使用父类或者Object的属性
  1. 继承注意事项: 不要使用对象字面量方法重写prototype,否则将切断继承关系

2.2 通过代码说明继承

代码目标:定义父类SuperType,定义子类SubType并继承父类的实例属性和原型属性

// 1. 定义一个父类型 SuperType
// 1.1 创建SuperType的构造函数,其中包含属性值property: true
function SuperType() {
  this.property = true
}
// 1.2 设置SuperType的原型对象,添加方法属性getSuperValue()
SuperType.prototype.getSuperValue = function () {
  return this.property
}

// 2. 定义一个子类型 SubType - 创建SubType的构造函数,其中包含属性值subProperty: false
function SubType() {
  this.subProperty: false
}

// 3. 继承:使子类SubType继承父类SuperType的方法和原型(通过构造函数SuperType)
SubType.prototype = new SuperType()

// 4. 为子类SubType的原型对象添加方法getSubValue()
/* 注意:
 * 添加方法尽量避免使用对象字面量方法重写
 * 原因:再次重写整个prototype,使其仅包含一个object的实例,所以再也无法调用SuperType中的属性和方法
 * 导致结果:会切断SubType和SuperType之间的联系
 */
SubType.prototype.getSubValue = function () {
  return this.subProperty
}

// 3. 创建SubType的实例,该实例继承了SuperType的方法和属性,所以该实例可以直接使用父类的方法
var subInstance = new SubType()
alert(subInstance.getSuperValue())   // true - 子类实例可以使用父类的方法获得父类属性
alert(subInstance.getSubValue())     // false - 子类实例可以使用自身的方法获得自身的属性值
// 注意:本质上,SuperType作为引用类型,也继承了Object,所以Object的属性和方法存在于SuperTye的原型对象中。从而构成原型链。
复制代码

图片说明:

image.png

2.3 判断原型与实例的关系:instanceofisPrototypeOf()

// 方法1: instanceof
alert(subInstance instanceof Object)    // true
alert(subInstance instanceof SubType)   // true
alert(subInstance instanceof SuperType) // true

// 方法2:isPrototypeOf()
alert(Object.prototype.isPrototypeOf(subInstance))    // true
alert(SubType.prototype.isPrototypeOf(subInstance))   // true
alert(SuperType.prototype.isPrototypeOf(subInstance)) // true
复制代码

2.4 原型链的问题 – 引用类型放在原型属性中引发的问题

问题的本质:引用类型存在于prototype中,因为prototype具有共享特性,导致的问题

参考:JavaScript学习(3) – 聊聊原型链- 2. 对象与原型 – 2.3.6节:成也原型,败也原型

  1. 包含引用类型的值的原型 – 父类实例属性中如果包含一个引用类型值,则通过继承后,会变成子类的原型属性
  2. 无法向父类构造函数传参 – 创建子类时,无法在不影响所有对象实例的情况下,向父类的构造函数中传参

结论:很少单独使用原型链(解决方法:借用构造函数)

// 创建父类,其中包含实例属性colors
function SuperType () {
  this.colors = ['red', 'green', 'blue']
}

// 创建子类
function SubType() {}

// 子类继承父类 - 父类中的实例属性colors会变成原型属性,成为共享属性
SubType.prototype = new SuperType()

var instance1 = new SubType()
var instance2 = new SuperType()

// 由于引用对象是原型属性,所以对子类继承到的引用对象进行改变,会影响到父类自身的实例属性
instance1.colors.push('black')
alert(instance1.colors) // 'red, green, blue, black'
alert(instance2.colors) // 'red, green, blue, black'
复制代码

3. 借用构造函数 constructor stealing

  1. 基本思想:在子类构造函数的内部调用父类构造函数完成继承 – 用call()apply()实现
  2. 优点:可以向父类构造函数传参
  3. 缺点:function都在构造函数中定义,因此缺乏函数复用性(构造函数的问题)- 解决方案:组合继承
// 创建父类,父类构造函数需传参
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
复制代码

4. 组合继承 combination inheritance

  1. 基本思想:使用原型链实现对原型属性和方法的集成,通过借用构造函数来实现对实例属性的集成,从而保证原型中的方法可以复用,又能保证每个实例有自己单独的属性
  2. 缺点: 调用两次父类构造函数
  3. 解决方案:寄生组合式继承(但要了解该方法,需要先学习原型继承和寄生式继承)
// 创建父类,包含实例属性和原型方法
function SuperType(name) {
  this.name = name
  this.colors = ['red', 'green', 'blue']
}
SuperType.prototype.sayName = function () {
  alert(this.name)
}

// 创建子类
function SubType (name, age) {
  // 继承实例属性(第一次调用父类构造函数)
  SuperType(this, name)
  
  // 创建子类实例属性
  this.age = age
} 

// 子类继承父类原型方法
// 1. 继承父类(第二次调用父类构造函数)
SubType.prototype = new SuperType()
// 2. 将继承后子类原型中的constructor重写,指回SubType本身的constructor,而不是让其默认指向父类SuperType的constructor
SubType.prototype.constructor = SubType
// 3. 给继承后的原型添加方法sayAge
SubType.prototype.sayAge = function () {
  alert(this.age)
}

// 创建一个子类型的实例instance1,其具备独立的实例属性:name、age、colors,其中name和colors继承于父类
var instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')
alert(instance1.colors) // 'red, green, blue, black'
instance1.sayName()     // Nicholas
instance1.sayAge()      // 29

// 创建另一个子类型实例,其他同类实例的改变不会对该实例造成影响,各个实例具备独立的实例属性
var instance2 = new SubType('Cindy', 27)
alert(instance2.colors) // 'red, green, blue'
instance2.sayName()     // Cindy
instance2.sayAge()      // 27
复制代码

5. 原型继承 Prototypal Inheritance

  1. 基本思想:浅拷贝原型及属性
  2. 缺点:该方法同样存在引用类型的问题,故不会直接使用,但是给了解决上面组合继承的问题的一个思路
// 将对象进行浅拷贝的方法:
function object(o) {
  function F ()   // 1. 创建临时构造函数
  F.prototype = o // 2. 将传入的对象o作为这个临时构造函数的原型
  return new F()  // 3. 返回该临时类型的实例
}

// 使用样例:
// 1. 创建一个普通对象,其中存在引用类型属性
var obj = {
  name: 'showColor',
  colors: ['red', 'green', 'blue']
}

// 2. 使用object()方法对obj对象进行浅拷贝 - obj对象作为新的实例的原型对象,故obj中原有属性现在均为原型属性进行共享
var obj2 = object(person)
obj2.name = 'anotherColor'
obj2.colors.push('black') // colors现在属于原型对象,所以colors现在是共享状态的,任何改变都会对原本的对象进行改变

alert(obj.colors) // 'red, green, blue, black'
复制代码
  1. 补充知识:ES5提供了Object.create()方法,可以传入两个参数
/*
 * Object.create(arg1, arg2)
 * 参数1:用作新对象原型的对象
 * 参数2:为新对象定义定义额外属性的对象(可重写覆盖原有同名属性)
 */
// 1. 创建一个普通对象
var obj = {
  name: 'showColor',
  colors: ['red', 'green', 'blue']
}

// 2. 只传第一个参数:使用官方的Object.create()方法对obj对象进行浅拷贝,此时obj的内容存在于obj2.prototype中
var obj2 = Object.create(obj)
obj2.name = 'anotherColor' // 为obj2添加实例属性name
obj2.colors.push('black')  // 为obj2添加属性,因colors存在于prototype中,且未引用类型(Array),所以是对引用对象修改
/* obj2 - 输出结果:
  {
    name: 'anotherColor',
    _proto_: {
      name: 'showColor',
      colors: ["red", "green", "blue", "black"]
    }
  }
*/
console.log(obj1.colors) // ["red", "green", "blue", "black"]

// 3. 传两个参数,对同名属性进行覆盖重写
var obj3 = Object.create(person, { 
	name: {
    value: 'color2'
  }
})
alert(obj3.name) // color2
复制代码

6. 寄生式继承 Parasitic Inheritance

  1. 基本思路:创建一个仅用于封装继承过程的函数,该函数内部以某种方式增强对象,然后按照工厂模式的思路,返回整个对象
  2. 本质:寄生构造函数 + 工场模式 + 原型继承
  3. 缺点:实例属性中增强的函数无法复用,问题类似构造函数模式
  4. 解决方案:寄生组合式继承
// 寄生继承函数,可将对象进行增强
function createAnother (original) {
  var clone = Object.create(original)              // 1. 浅拷贝传入的对象
  clone.sayHi = function () { alert('Hi') }        // 2. 增强对象(例如:写入其他方法,放入实例属性中)
  return clone                                     // 3. 返回这个对象
}

// 创建对象
var obj = {
  name: 'showColor',
  colors: ['red', 'green', 'blue']
}

// 寄生继承上面的对象,新对象可以使用增强的内容
var obj2 = createAnother(obj)
obj2.sayHi() // hi
复制代码

7. 寄生组合式继承 – 最佳解决方案

7.1 简单总结上面几种继承方法的问题

继承方法 基本思路 问题
直接使用原型链 将父类对象直接继承到子类prototype中 1. 父类实例属性中的引用对象会被继承到子类原型对象中,对子类该属性的修改会影响父类的该实例属性
2. 无法向父类构造函数传参
解决方案:借用构造函数
借用构造函数 1. 将父类的属性继承到构造函数中作为子类的实例属性
2. 可以对父类构造函数传参
无法将需要共享的属性(例如function)继承到prototype中,缺乏函数复用性
解决方案:组合继承
组合继承 1. 构造函数部分继承父类的实例属性
2. 原型部分继承父类的原型属性
调用两次父类构造函数:
1. 创建子类构造函数时,使用第一次父类构造函数
2. 继承父类到子类prototype中
解决方案:寄生组合式继承(但须先了解原型继承+寄生继承)
原型继承 浅拷贝原型属性到子类prototype中 存在同直接使用原型链一样的问题
寄生式继承 原型模式 + 寄生构造函数:
1. 浅拷贝父类对象到子类prototype中
2. 对拷贝后的副本对象写入实例属性
实例属性中增强的函数无法复用,问题类似构造函数模式
寄生组合式继承 组合继承 + 寄生继承
使用寄生式继承来继承父类原型,再将结果指定给子类原型

7.2 寄生组合式继承方法

  1. 基本思路:就是使用寄生式继承来继承父类原型,再将结果指定给子类原型
  2. 本质:组合继承 + 寄生继承
  3. 优点:仅调用一次父类构造函数、可以传参,使用寄生继承的方式将父类prototype指定给子类prototype
  4. 继承操作逻辑
    1. 创建父类,包含实例属性和原型属性
    2. 创建子类构造函数,调用父类构造函数将父类的实例属性写入子类实例属性,并可添加自身实例属性
    3. 寄生继承父类原型对象:
      1. 浅拷贝父类prototype
      2. 增强浅拷贝副本,用子类构造函数覆盖副本的constructor(避免父类实例属性存在于子类原型中)
      3. 将增强后的父类prototype副本存入子类prototype
    4. 为子类prototype添加自身原型属性
// 寄生组合继承方法:
function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype) // 1. 创建父类prototype的副本对象
  prototype.constructor = subType                    // 2. 增强副本对象,将子类构造函数添加覆盖
  subType.prototype = prototype                      // 3. 指定子类对象的原型为上面创建的副本对象
}

// 使用上述方法进行继承:
// 1. 创建父类,并添加原型方法sayName()
function SuperType(name) {
  this.name = name
  this.colors = ['red', 'green', 'blue']
}
SuperType.prototype.sayName = function() {
  alert(this.name)
}

// 2. 创建子类 - 通过寄生继承的方式,继承原型对象
function SubType(name, age) {
  // 调用一次父类构造函数,传入父类的实例属性
  SuperType.call(this, name) 
  this.age = age
}
// 使用寄生继承的方式,通过浅拷贝父类的原型对象,直接将副本指定给子类的原型对象,避免在SubType.prototype上创建不必要的属性
inheritPrototype(SubType, SuperType)  
SubType.prototype.sayAge = function () {
  alert(this.age)
}
复制代码

8. 小结

  1. JavaScript:仅支持实现继承,且不支持重载(可用Java继承思想来理解)
  2. 原型链:父类被继承到子类的prototype中,层层溯源,直接使用
  3. 原型链的问题:引用对象放在protoype的共享问题
  4. 几种继承的方法:(基本思路 + 优缺点 + 解决方案)
    1. 直接使用原型链
    2. 借用构造函数
    3. 组合继承
    4. 原型继承
    5. 寄生式继承(寄生的本质:增强 – 在原有基础上添加自定义新属性)
    6. 寄生组合式继承(最佳)
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享