《javascript高级程序设计》学习笔记 | 8.3.继承
发布于 3 年前 作者 simon9124 1719 次浏览 来自 分享

关注前端小讴,阅读更多原创技术文章

继承

  • 面向对象语言支持 2 种继承方式:接口继承实现继承
  • JS 函数没有签名(不必提前声明变量的类型),只支持实现继承,依靠原型链

相关代码→

原型链

  • 子类型构造函数的原型,被重写为超类型构造函数的实例
function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}
function SubType() {}
SubType.prototype = new SuperType() // SubType的原型 = SuperType的实例,SubType原型被重写 → SubType 继承了 SuperType

console.log(SubType.prototype.__proto__) // SuperType原型,SuperType实例的[[Prototype]]指向SuperType原型
console.log(SubType.prototype.__proto__.constructor) // SuperType构造函数,SuperType原型的constructor指向SuperType构造函数
  • 超类型实例属性和方法,均存在于子类型的原型
  • 子类型的实例可访问超类型原型上的方法,方法仍存在于超类型的原型中
var instance = new SubType()
console.log(instance.property) // true,SubType继承了property属性
console.log(SubType.prototype.hasOwnProperty('property')) // true,property是SuperType的实例属性,SubType的原型已被重写为SuperType的实例
console.log(instance.getSuperValue()) // true,SubType继承了getSuperValue()方法
console.log(SubType.prototype.hasOwnProperty('getSuperValue')) // false,getSuperValue是SuperType的原型方法,不存在于SubType的实例中
console.log(SuperType.prototype.hasOwnProperty('getSuperValue')) // true
  • 调用子类型构造函数创建实例后,由于子类型的原型被重写
    • 子类实例的[[Prototype]]指向超类实例(原本指向子类原型)
    • 子类实例的constructor指向重写子类原型的构造函数,即超类构造函数(原本指向子类构造函数)
  • 谁(哪个构造函数)重写了原型,(实例和原型的)constructor 就指向谁
console.log(instance.__proto__) // SuperType实例,SubType的原型SubType.prototype已被SuperType的实例重写
console.log(instance.constructor) // SuperType构造函数,constructor指向重写原型对象的constructor,即new SuperType()的constructor
console.log(instance.constructor === SubType.prototype.constructor) // true,都指向SuperType构造函数
  • 实现了原型链后,代码读取对象属性的搜索过程:

    • 1.搜索对象实例本身 -> 有属性 → 返回属性值 -> 结束
    • 2.对象实例本身无属性 -> 搜索原型对象 → 有属性 → 返回属性值 -> 结束
    • 3.原型对象无属性 -> 一环一环向上搜索原型链 → 有/无属性 → 返回属性值/undefined → 结束

默认原型

  • 所有引用类型都默认继承Object,所有函数的默认原型都是 Object 实例
  • 默认原型内部的[[Prototype]]指针,指向Object的原型即Object.prototype
  • Object.prototype上保存着constructor、hasOwnProperty、isPrototypeOf、propertyIsEnumerable、toString、valueOf、toLocaleString等默认方法,在实例中调用这些方法时,其实调用的是 Object 原型上的方法
console.log(SuperType.prototype.__proto__) // {},SuperType的默认原型是Object实例,Object实例的[[Prototype]]指向Object原型
console.log(SuperType.prototype.__proto__ === Object.prototype) // true,都指向Object原型
console.log(SuperType.prototype.__proto__.constructor) // Object构造函数
console.log(Object.keys(SuperType.prototype.__proto__)) // [],Object原型上可枚举的方法
console.log(Object.getOwnPropertyNames(SuperType.prototype.__proto__)) // [ 'constructor','__defineGetter__','__defineSetter__','hasOwnProperty','__lookupGetter__','__lookupSetter__','isPrototypeOf','propertyIsEnumerable','toString','valueOf','__proto__','toLocaleString' ],Object原型上的所有方法

原型与继承关系

  • instanceof操作符,测试实例原型链中出现过的构造函数
  • instanceof具体含义:判断一个构造函数的 prototype 属性所指向的对象,是否存在于要检测对象(实例)原型链
console.log(instance instanceof Object) // true,instance是Object的实例
console.log(instance instanceof SuperType) // true,instance是SuperType的实例
console.log(instance instanceof SubType) // true,instance是SubType的实例
  • isPrototypeOf()方法,测试实例原型链上的原型
  • isPrototypeOf()具体含义:判断一个对象(原型对象)是否存在于要检测对象(实例)原型链
console.log(Object.prototype.isPrototypeOf(instance)) // true,Object.prototype是instance原型链上的原型
console.log(SuperType.prototype.isPrototypeOf(instance)) // true,SuperType.prototype是instance原型链上的原型
console.log(SubType.prototype.isPrototypeOf(instance)) // true,SubType.prototype是instance原型链上的原型

关于方法

  • 在子类型原型添加或重写超类型方法的代码,一定要放在替换原型语句之后
SubType.prototype.getSubValue = function () {
  // 给子类原型添加新方法
  return false
}
SubType.prototype.getSuperValue = function () {
  // 在子类原型中重写超类原型的方法
  return false
}
var instance2 = new SubType()
console.log(instance2.getSubValue()) // false
console.log(instance2.getSuperValue()) // false,方法被重写
var instance3 = new SuperType()
console.log(instance3.getSuperValue()) // true,不影响超类型原型中的方法
  • 通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链,导致继承关系失效
function SubType2() {}
SubType2.prototype = new SuperType() // 继承

SubType2.prototype = {
  // 对象字面量重写原型,继承关系失效(子类原型被重写为Object实例)
  someFunction: function () {
    return false
  },
}
var instance4 = new SubType2()
console.log(instance4.getSuperValue()) // error,对象字面量重写了原型,继承关系已失效

原型链的问题

  • 子类实例引用类型的属性进行修改(非重新定义)时,会对超类实例的引用类型属性造成影响
function SuperTypePro(name) {
  this.nums = [1, 2, 3] // 超类属性,引用类型
  this.name = name // 超类属性,原始类型
}
SuperTypePro.prototype.getSuperNums = function () {
  return this.nums
}
function SubTypePro() {}
SubTypePro.prototype = new SuperTypePro() // 继承

var instance5 = new SubTypePro()
instance5.nums.push(4) // 在子类实例中,修改(非重新定义)继承的引用类型属性
console.log(instance5.nums) // [1,2,3,4]
var instance6 = new SubTypePro()
console.log(instance6.nums) // [1,2,3,4],超类实例受到影响
var instance7 = new SubTypePro()
instance7.nums = [] // 在子类实例中,重新定义(覆盖)继承的引用类型属性
console.log(instance7.nums) // []
console.log(instance6.nums) // [1,2,3,4],超类实例不受影响
  • (在不影响所有对象实例的情况下)创建子类型实例时,无法给超类型构造函数传递参数
var person = new SuperTypePro('Simon') // 创建超类型实例
console.log(person.name) // 'Simon'
var person2 = new SubTypePro('Simon') // 创建子类型实例,参数传递无意义
console.log(person2.name) // undefined

盗用构造函数

  • 在子类构造函数内部,通过apply()call()超类构造函数作用域绑定给子类的实例 this,再调用超类构造函数
function SuperTypeBorrow() {
  this.nums = [1, 2, 3]
}
function SubTypeBorrow() {
  console.log(this) // SubTypeBorrow构造函数内部的this,指向SubTypeBorrow的实例
  SuperTypeBorrow.call(this) // 将超类的作用域绑定给this,即子类的实例
}
var instance8 = new SubTypeBorrow()
console.log(instance8.nums) // [ 1, 2, 3 ]

instance8.nums.push(4)
console.log(instance8.nums) // [ 1, 2, 3, 4 ]
var instance9 = new SubTypeBorrow()
console.log(instance9.nums) // [ 1, 2, 3 ],超类不受影响

传递参数

  • 可以在子类构造函数中,向超类构造函数传递参数
  • 为确保超类构造函数不会重写子类的属性,先调用超类构造函数,再添加子类中定义的属性
function SuperTypeParam(name) {
  this.name = name
}
function SubTypeParam() {
  SuperTypeParam.call(this, 'Nicholas') // 继承,先调用超类型构造函数
  this.age = 29 // 再添加子类型中定义的属性
}
var instance10 = new SubTypeParam()
console.log(instance10.name, instance10.age) // Nicholas 29

盗用构造函数的问题

  • 构造函数模式存在的问题 —— 方法都在超类构造函数中定义,每个方法都会在实例上创建一遍,函数没有复用,且超类原型中定义的方法,在子类中不可见

组合继承

  • 又称伪经典继承,使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性
  • 既通过超类原型上定义的方法实现了函数复用,又保证每个实例有自己的属性
/* 超类型构造函数 */
function SuperTypeMix(name) {
  this.name = name
  this.nums = [1, 2, 3]
}
SuperTypeMix.prototype.sayName = function () {
  console.log(this.name)
}
/* 子类型构造函数 */
function SubTypeMix(name, age) {
  SuperTypeMix.call(this, name) // 盗用构造函数继承,继承实例属性
  this.age = age
}
SubTypeMix.prototype = new SuperTypeMix() // 原型链继承,继承原型方法
SubTypeMix.prototype.sayAge = function () {
  console.log(this.age) // 子类型原型添加方法(须在替换原型语句之后)
}

var instance11 = new SubTypeMix('Nicholas', 29)
instance11.nums.push(4)
console.log(instance11.nums) // [ 1, 2, 3, 4 ],盗用构造函数继承而来,属性保存在超类型实例和子类型原型中
instance11.sayName() // 'Nicholas',原型链继承而来,方法保存在超类型原型中
instance11.sayAge() // 29,非继承,方法保存在子类型原型中

var instance12 = new SubTypeMix('Greg', 27)
console.log(instance12.nums) // [ 1, 2, 3]
instance12.sayName() // 'Greg'
instance12.sayAge() // 27
  • 组合继承也有自己的不足,其会调用 2 次超类构造函数
    • 第一次,是在重写子类原型时,超类实例属性赋给子类原型
    • 第二次,是在调用子类构造函数创建子类实例时,超类实例属性赋给子类实例
/* 超类构造函数 */
function SuperTypeMix(name) {
  this.name = name
  this.nums = [1, 2, 3]
}
/* 子类构造函数 */
function SubTypeMix(name) {
  SuperTypeMix.call(this, name) // 盗用构造函数继承,继承属性(创建子类实例时,第二次调用超类构造函数,子类实例继承超类实例属性)
}
SubTypeMix.prototype = new SuperTypeMix() // 原型链继承,继承方法(第一次调用超类构造函数,子类原型已经继承了超类实例和原型中的方法和属性)
  • 调用 2 次超类型构造函数影响效率,且:
    • 子类原型子类实例上,都继承并包含了超类实例属性
    • 子类原型上的超类实例属性会被子类实例的同名属性覆盖,因此子类原型上的是不必要
    • 从子类实例删除继承自超类实例的属性,属性仍存在于子类原型中,仍然可以被访问到
var instance11 = new SubTypeMix('Nicholas') // 创建子类实例
instance11.nums.push(4)

console.log(SubTypeMix.prototype) // SuperTypeMix { name: undefined, nums: [ 1, 2, 3 ], sayAge: [Function] },子类原型(被重写为超类实例)
console.log(instance11) // SuperTypeMix { name: 'Nicholas', nums: [ 1, 2, 3, 4 ], age: 29 },子类实例
delete instance11.nums // 从子类实例中删除(继承自超类实例的)属性
console.log(instance11) // SuperTypeMix { name: 'Nicholas', age: 29 },子类实例
console.log(instance11.nums) // [ 1, 2, 3 ],仍然可以(从子类原型中)访问到该属性

原型式继承

  • 创建一个函数,接收一个参数对象(必传)
    • 在函数内部创建临时构造函数
    • 将传入的对象作为这个构造函数的原型
    • 返回这个构造函数的新实例
  • 从本质上讲,该函数对传入其中的对象执行了一次浅复制
function object(o) {
  function F() {} //函数内部创建临时构造函数
  F.prototype = o // 将传入的对象作为这个构造函数的原型
  return new F() // 返回这个构造函数的新实例
}
  • 传入的对象作为另一个对象的基础,是函数返回的新对象的原型,其属性值(基本类型值 & 引用类型值)被新对象所共享
  • 返回的新对象相当于传入的对象创建的副本
var person = {
  name: 'Nicholas',
}
var anotherPerson = object(person)
console.log(anotherPerson.name) // 'Nicholas',来自person
console.log(anotherPerson.hasOwnProperty('name')) // false
anotherPerson.name = 'Greg' // 覆盖同名属性
console.log(anotherPerson.hasOwnProperty('name')) // true
console.log(anotherPerson.name) // 'Greg',来自副本
console.log(person.name) // 'Nicholas',来自person
  • ES5 的Object.create()方法规范化原型式继承,接收 2 个参数
    • 参数一:用作新对象原型的对象,必传
    • 参数二:为新对象定义额外属性的对象,非必传
  • 不传第二个参数时Object.create()方法与前面提到的object()函数的行为相同
var anotherPerson2 = Object.create(person)
console.log(anotherPerson2.name) // 'Nicholas',来自person
  • 第二个参数与 Object.defineProperties()——定义对象属性方法——的第二个参数格式相同,通过描述符定义要返回的对象的属性
var anotherPerson3 = Object.create(person, {
  name: { value: 'Greg' }, // 描述符定义对象的属性,若有同名属性则覆盖
})
console.log(anotherPerson3.name) // 'Greg',来自副本
  • 无需创建构造函数、只是想让一个对象与另一个对象保持类似的情况下,可使用原型式继承
  • 原型模式创建对象,作为原型的对象的引用类型属性始终被作为原型的对象和副本共享,修改(非重新定义)副本中引用类型的值,会对作为原型的对象的引用类型属性造成影响
var person2 = {
  nums: [1, 2, 3],
}
var anotherPerson4 = Object.create(person2)
anotherPerson4.nums.push(4) // 引用类型属性被修改,非重新定义
console.log(anotherPerson4.nums) // [1, 2, 3, 4],来自person
console.log(person2.nums) // [1, 2, 3, 4],作为原型的引用类型属性受到影响

寄生式继承

  • 原型式继承紧密相关,其思路与寄生构造函数工厂模式类似:
    • 创建一个仅用于封装继承过程的函数,接收一个参数,参数是作为原型的对象
    • 在函数内部,调用原型式继承封装的函数,返回一个实例对象,再以某种方式增强这个实例对象
    • 最后返回这个实例对象
function createAnother(original) {
  var clone = Object.create(original) // 进行原型式继承,返回一个空实例
  console.log(clone) // {},空实例,其原型是orginal对象
  clone.sayHi = function () {
    console.log('Hi') // 给返回的实例对象添加方法(每个实例重新创建方法)
  }
  return clone
}

var person3 = {
  name: 'Nicholas',
}
var anotherPerson5 = createAnother(person3)

console.log(anotherPerson5.name) // 'Nicholas'
console.log(anotherPerson5.hasOwnProperty('name')) // false,name属性保存在作为原型的对象person3上
anotherPerson5.sayHi() // 'Hi'
console.log(anotherPerson5.hasOwnProperty('sayHi')) // true,sayHi方法保存在返回的实例对象上
console.log(anotherPerson5) // { sayHi: [Function] }
  • 主要考虑对象而不是自定义类型和构造函数的情况下,可使用原型式继承
  • 构造函数模式存在的问题 —— 方法都在寄生式继承的封装函数中定义,无法做到方法复用而降低了效率

寄生组合式继承

  • 原型链的混成形式:
    • 不通过调用超类构造函数给子类原型赋值(重写),只需超类原型的副本
    • 使用寄生式继承来继承超类的原型,再将结果指定给子类的原型
    • 核心:子类的原型继承超类的原型
// 封装:原型链的混成形式
function inherit(subType, superType) {
  // 1.创建对象,继承超类的原型
  var superPrototype = Object.create(superType.prototype) // superPrototype的原型是超类原型
  console.log(superPrototype.__proto__) // 指向superType.prototype超类原型
  console.log(superPrototype.__proto__ === superType.prototype) // true
  console.log(superPrototype.constructor) // 此时constructor指向超类构造函数
  // 2.让constructor指向子类构造函数
  superPrototype.constructor = subType
  // 3.将创建的对象赋给子类的原型
  subType.prototype = superPrototype
  console.log(subType.prototype.__proto__ === superType.prototype) // true,子类原型继承超类原型
}
  • 使用盗用构造函数继承实例属性,通过原型链的混成形式继承原型方法
/* 超类 */
function SuperTypeMixParasitic(name) {
  this.name = name
  this.nums = [1, 2, 3]
}
SuperTypeMixParasitic.prototype.sayName = function () {
  console.log(this.name)
}
/* 子类 */
function SubTypeMixParasitic(name, age) {
  SuperTypeMixParasitic.call(this, name) // 盗用构造函数,继承属性(只调用1次超类构造函数)
  this.age = age
}

inherit(SubTypeMixParasitic, SuperTypeMixParasitic) // 原型链的混成形式,继承方法
SubTypeMixParasitic.sayAge = function () {
  console.log(this.age)
}
  • 寄生组合式继承是引用类型最理想的继承范式

    • 只调用 1 次超类构造函数,不会在子类原型上创建多余的属性
    var instance13 = new SubTypeMixParasitic('Nicholas', 29)
    instance13.nums.push(4)
    console.log(instance13.nums) // [ 1, 2, 3, 4 ],盗用构造函数继承而来,属性保存在子类实例([ 1, 2, 3, 4 ])和超类实例([ 1, 2, 3 ])中
    console.log(SubTypeMixParasitic.prototype) // SubTypeMixParasitic { constructor: { [Function: SubTypeMixParasitic] sayAge: [Function] } },子类原型不含多余属性,只继承超类原型的方法,且constructor指向子类构造函数
    
    • 原型链保持不变
    console.log(SubTypeMixParasitic.prototype.constructor) // SubTypeMixParasitic构造函数
    console.log(instance13.__proto__ === SubTypeMixParasitic.prototype) // true
    
    console.log(SubTypeMixParasitic.prototype.__proto__) // SuperTypeMixParasitic原型
    console.log(
      SubTypeMixParasitic.prototype.__proto__ === SuperTypeMixParasitic.prototype
    ) // true
    console.log(SubTypeMixParasitic.prototype.__proto__.constructor) // SuperTypeMixParasitic构造函数
    
    console.log(SubTypeMixParasitic.prototype.__proto__.__proto__) // Object原型
    console.log(
      SubTypeMixParasitic.prototype.__proto__.__proto__ === Object.prototype
    ) // true
    console.log(SubTypeMixParasitic.prototype.__proto__.__proto__.constructor) // Object构造函数
    
    • 能正常使用instanceofisPrototypeOf()——因为constructor仍旧指向子类型构造函数
    console.log(instance13 instanceof SubTypeMixParasitic) // instance13是SubTypeMixParasitic的实例
    console.log(instance13 instanceof SuperTypeMixParasitic) // instance13是SuperTypeMixParasitic的实例
    console.log(SubTypeMixParasitic.prototype.isPrototypeOf(instance13)) // true,SubTypeMixParasitic.prototype是instance13原型链上的原型
    console.log(SuperTypeMixParasitic.prototype.isPrototypeOf(instance13)) // true,SuperTypeMixParasitic.prototype13是instance原型链上的原型
    

总结 & 问点

  • 什么是函数签名?为什么 JS 函数没有签名?JS 支持哪种方式的继承?其依靠是什么?
  • 原型链继承的原理是什么?超类型实例上的属性和方法保存在哪些位置?超类型原型上的方法呢?
  • 通过原型链实现继承时,调用子类型构造函数创建实例后,由于子类型的原型被重写,实例的[[Prototype]]和 constructor 指针发生了怎样的变化?为什么?
  • 通过原型链实现继承后,代码读取对象属性的搜索过程是什么?
  • 所有引用类型都默认继承自什么?所有函数的默认原型都是什么?默认原型内部的[[Prototype]]指向哪里?
  • 在实例中调用 toString()、valueOf()等常用方法时,实际调用的是哪里的方法?
  • 有哪些方法可以确定原型和实例的关系?其分别含义和用法是什么?
  • 通过原型链实现继承时,为什么给子类原型添加或覆盖超类方法必须在替换原型语句之后?为什么不能使用对象字面量创建子类原型方法?
  • 单独使用原型链实现继承有哪些局限?
  • 盗用构造函数继承的原理是什么?相比原型链继承有什么优势?其缺点又是什么?
  • 组合继承的原理是什么?作为最常用的继承模式,其有哪些优势和缺点?
  • 原型式继承的原理是什么?在什么情况下可以使用这种继承方式?其又有什么缺点?
  • 寄生式继承的原理是什么?在什么情况下可以使用这种继承方式?其又有什么缺点?
  • 请用代码完整展示寄生组合式继承的过程,并说说为什么它是“引用类型最理想的继承范式”?
2 回复

根据 函数的签名 的定义来说,JS 也是有函数签名的,只不过解释成是默认隐式的(implicit)比较严谨。即使是动态类型,对于某个确定的函数或者方法而言,它能够被合法使用的范围是确定的(除非有个方法,能接受任意输入,并在任何场景下都返回调用方认为正确的输出),而签名就是描述该范围的,可以认为 JS 在语法上不支持显式地描述函数的签名,但是签名本身是存在的,依然可以通过 JSDoc 之类的形式来标注

@hsiaosiyuan0 明白了,我这研究得还不够深入,谢谢大佬分享~~

回到顶部