《javascript高级程序设计》学习笔记 | 8.3.继承
关注前端小讴,阅读更多原创技术文章
继承
- 面向对象语言支持 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构造函数
- 能正常使用
instanceof
和isPrototypeOf()
——因为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 明白了,我这研究得还不够深入,谢谢大佬分享~~