共计 12309 个字符,预计需要花费 31 分钟才能阅读完成。
关注前端小讴,浏览更多原创技术文章
继承
- 面向对象语言反对 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()等罕用办法时,理论调用的是哪里的办法?
- 有哪些办法能够确定原型和实例的关系?其别离含意和用法是什么?
- 通过原型链实现继承时,为什么给子类原型增加或笼罩超类办法必须在替换原型语句之后?为什么不能应用对象字面量创立子类原型办法?
- 独自应用原型链实现继承有哪些局限?
- 盗用构造函数继承的原理是什么?相比原型链继承有什么劣势?其毛病又是什么?
- 组合继承的原理是什么?作为最罕用的继承模式,其有哪些劣势和毛病?
- 原型式继承的原理是什么?在什么状况下能够应用这种继承形式?其又有什么毛病?
- 寄生式继承的原理是什么?在什么状况下能够应用这种继承形式?其又有什么毛病?
- 请用代码残缺展现寄生组合式继承的过程,并说说为什么它是“援用类型最现实的继承范式”?
正文完
发表至: javascript
2021-05-25