《javascript高級程序設計》學習筆記 | 8.3.繼承

關注前端小謳,閱讀更多原創技術文章

繼承

  • 面嚮對象語言支持 2 種繼承方式:接口繼承實現繼承
  • JS 函數沒有簽名(沒必要提早聲明變量的類型),只支持實現繼承,依靠原型鏈

相關代碼 →javascript

原型鏈

  • 子類型構造函數的原型,被重寫爲超類型構造函數的實例
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構造函數
  • 實現了原型鏈後,代碼讀取對象屬性的搜索過程:java

    • 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 次超類構造函數git

    • 第一次,是在重寫子類原型時,超類實例屬性賦給子類原型
    • 第二次,是在調用子類構造函數建立子類實例時,超類實例屬性賦給子類實例
/* 超類構造函數 */
function SuperTypeMix(name) {
  this.name = name
  this.nums = [1, 2, 3]
}
/* 子類構造函數 */
function SubTypeMix(name) {
  SuperTypeMix.call(this, name) // 盜用構造函數繼承,繼承屬性(建立子類實例時,第二次調用超類構造函數,子類實例繼承超類實例屬性)
}
SubTypeMix.prototype = new SuperTypeMix() // 原型鏈繼承,繼承方法(第一次調用超類構造函數,子類原型已經繼承了超類實例和原型中的方法和屬性)
  • 調用 2 次超類型構造函數影響效率,且:github

    • 子類原型子類實例上,都繼承幷包含了超類實例屬性
    • 子類原型上的超類實例屬性會被子類實例的同名屬性覆蓋,所以子類原型上的是沒必要要
    • 從子類實例刪除繼承自超類實例的屬性,屬性仍存在於子類原型中,仍然能夠被訪問到
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 ],仍然能夠(從子類原型中)訪問到該屬性

原型式繼承

  • 建立一個函數,接收一個參數對象(必傳)app

    • 在函數內部建立臨時構造函數
    • 將傳入的對象做爲這個構造函數的原型
    • 返回這個構造函數的新實例
  • 從本質上講,該函數對傳入其中的對象執行了一次淺複製
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] }
  • 主要考慮對象而不是自定義類型和構造函數的狀況下,可以使用原型式繼承
  • 構造函數模式存在的問題 —— 方法都在寄生式繼承的封裝函數中定義,沒法作到方法複用而下降了效率

寄生組合式繼承

  • 原型鏈的混成形式:this

    • 不經過調用超類構造函數給子類原型賦值(重寫),只需超類原型的副本
    • 使用寄生式繼承來繼承超類的原型,再將結果指定給子類的原型
    • 核心:子類的原型繼承超類的原型
// 封裝:原型鏈的混成形式
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)
}
  • 寄生組合式繼承是引用類型最理想的繼承範式prototype

    • 只調用 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()等經常使用方法時,實際調用的是哪裏的方法?
  • 有哪些方法能夠肯定原型和實例的關係?其分別含義和用法是什麼?
  • 經過原型鏈實現繼承時,爲何給子類原型添加或覆蓋超類方法必須在替換原型語句以後?爲何不能使用對象字面量建立子類原型方法?
  • 單獨使用原型鏈實現繼承有哪些侷限?
  • 盜用構造函數繼承的原理是什麼?相比原型鏈繼承有什麼優點?其缺點又是什麼?
  • 組合繼承的原理是什麼?做爲最經常使用的繼承模式,其有哪些優點和缺點?
  • 原型式繼承的原理是什麼?在什麼狀況下可使用這種繼承方式?其又有什麼缺點?
  • 寄生式繼承的原理是什麼?在什麼狀況下可使用這種繼承方式?其又有什麼缺點?
  • 請用代碼完整展現寄生組合式繼承的過程,並說說爲何它是「引用類型最理想的繼承範式」?
相關文章
相關標籤/搜索