JavaScript物件導向—繼承的實現

MomentYY發表於2022-03-13

JavaScript物件導向—繼承的實現

前言

物件導向的三大特性:封裝、繼承和多型。上一篇我們簡單的瞭解了封裝的過程,也就是把物件的屬性和方法封裝到一個函式中,這一篇講一下JavaScript中繼承的實現,繼承是物件導向中非常重要的特性,它可以幫助我們提高程式碼的複用性。繼承主要的思想就是將重複的程式碼邏輯抽取到分類中,子類只需要通過繼承分類,就可以使用分類中的方法,但是在實現JavaScript繼承之前,需要先了解一個重要的知識點“原型鏈”。

1.JavaScript中的原型鏈

在上一篇JavaScript物件導向—物件的建立和操作中已經簡單的瞭解過了JavaScript中物件的原型和函式的原型,當我們從一個物件上獲取屬性時,如果在當前物件自身沒有找到該屬性的話,就會去它原型上面獲取,如果原型中也沒有找到就會去它原型的原型上找,沿著這麼一條線進行查詢,那麼這條線就是我們所說的原型鏈了。

示例程式碼:

const obj = {
  name: 'curry',
  age: 30
}

obj.__proto__ = {}
obj.__proto__.__proto__ = {}
obj.__proto__.__proto__.__proto__ = { height: 1.83 }

console.log(obj.height) // 1.83

對應的記憶體中的查詢過程:

當通過原型鏈查詢某個屬性時,一直找不到的話會一直查詢下去麼?肯定是不會的,JavaScript的原型鏈也是有盡頭的,這個盡頭就是Object的原型。

2.Object的原型

事實上,不管是物件還是函式,它們原型鏈的盡頭都是Object的原型,也稱之為頂層原型,我們可以列印看看這個頂層原型長什麼樣。

(1)列印Object的原型

console.log(Object.prototype)
  • 在node環境中:

  • 在瀏覽器中:

(2)Object原型的特殊之處

  • 如果我們再次列印Object.prototype的原型,這個原型屬性已經指向了null

    console.log(Object.prototype.__proto__) // null
    
  • 並且在Object.prototype上有很多預設的屬性和方法,像toString、hasOwnProperty等;

(3)上一篇中講到當使用new操作符呼叫建構函式時,其物件的[[prototype]]會指向該建構函式的原型prototype,其實Object也是一個建構函式,因為我們可以使用new操作符來呼叫它,建立一個空物件。

  • 示例程式碼:

    const obj = new Object()
    
    obj.name = 'curry'
    obj.age = 30
    
    console.log(obj.__proto__ === Object.prototype) // true
    console.log(obj.__proto__) // [Object: null prototype] {}
    console.log(obj.__proto__.__proto__) // null
    
  • 記憶體表現:

(4)總結

  • 從Object的原型可以得出一個結論“原型鏈最頂層的原型物件就是Object的原型物件”,這也就是為什麼所有的物件都可以呼叫toString方法了;
  • 從繼承的角度來講就是“Object是所有類的父類”;

3.JavaScript繼承的實現方案

3.1.方案一:通過原型鏈實現繼承

如果需要實現繼承,那麼就可以利用原型鏈來實現了。

  • 定義一個父類Person和子類Student
  • 父類中存放公共的屬性和方法供子類使用;
  • 核心:將父類的例項化物件賦值給子類的原型;
// 定義Person父類公共的屬性
function Person() {
  this.name = 'curry'
  this.age = 30
}
// 定義Person父類的公共方法
Person.prototype.say = function() {
  console.log('I am ' + this.name)
}

// 定義Student子類特有的屬性
function Student() {
  this.sno = 101111
}

// 實現繼承的核心:將父類的例項化物件賦值給子類的原型
Student.prototype = new Person()

// 定義Student子類特有的方法
Student.prototype.studying = function() {
  console.log(this.name + ' studying')
}

// 例項化Student
const stu = new Student()
console.log(stu.name) // curry
console.log(stu.age) // 30
console.log(stu.sno) // 101111
stu.say() // I am curry
stu.studying() // curry studying

記憶體表現:

缺點

  • 從記憶體表現圖中就可以看出,當列印stu物件時,name和age屬性是看不到的,因為不會列印原型上的東西;
  • 當父類中的屬性為引用型別時,子類的多個例項物件會共用這個引用型別,如果進行修改,會相互影響;
  • 在使用該方案實現繼承時,屬性都是寫死的,不支援動態傳入引數來定製化屬性值;

3.2.方案二:借用建構函式實現繼承

針對方案一的缺點,可以借用建構函式來進行優化。

  • 在子類中通過call呼叫父類,這樣在例項化子類時,每個例項就可以建立自己單獨屬性了;
// 定義Person父類公共的屬性
function Person(name, age) {
  this.name = name
  this.age = age
}
// 定義Person父類的公共方法
Person.prototype.say = function() {
  console.log('I am ' + this.name)
}

// 定義Student子類特有的屬性
function Student(name, age, sno) {
  // 通過call呼叫Person父類,建立自己的name和age屬性
  Person.call(this, name, age)
  this.sno = sno
}

// 實現繼承的核心:將父類的例項化物件賦值給子類的原型
Student.prototype = new Person()

// 定義Student子類特有的方法
Student.prototype.studying = function() {
  console.log(this.name + ' studying')
}

// 例項化Student
const stu1 = new Student('curry', 30, 101111)
const stu2 = new Student('kobe', 24, 101112)
console.log(stu1) // Person { name: 'curry', age: 30, sno: 101111 }
console.log(stu2) // Person { name: 'kobe', age: 24, sno: 101112 }

記憶體表現:

缺點:

  • 在實現繼承的過程中,Person建構函式被呼叫了兩次,一次在new Person(),一次在Person.call()
  • 在Person的例項化物件上,也就是stu1和stu2的原型上,多出來了沒有使用的屬性name和age;

3.3.方案三:寄生組合式繼承

通過上面兩種方案,我們想實現繼承的目的是重複利用另外一個物件的屬性和方法,如果想解決方案二中的缺點,那麼就要減少Person的呼叫次數,避免去執行new Person(),而解決的辦法就是可以新增一個物件,讓該物件的原型指向Person的原型即可。

(1)物件的原型式繼承

將物件的原型指向建構函式的原型的過程就叫做物件的原型式繼承,主要可以通過以下三種方式實現:

  • 封裝一個函式,將傳入的物件賦值給建構函式的原型,最後將建構函式的例項化物件返回;

    function createObj(o) {
      // 定義一個Fn建構函式
      function Fn() {}
      // 將傳入的物件賦值給Fn的原型
      Fn.prototype = o
      // 返回Fn的例項化物件
      return new Fn()
    }
    
    const protoObj = {
      name: 'curry',
      age: 30
    }
    
    const obj = createObj(protoObj) // 得到的obj物件的原型已經指向了protoObj
    console.log(obj.name) // curry
    console.log(obj.age) // 30
    console.log(obj.__proto__ === protoObj) // true
    
  • 改變上面方法中的函式體實現,使用Object.setPrototypeOf()方法來實現,該方法設定一個指定的物件的原型到另一個物件或null;

    function createObj(o) {
      // 定義一個空物件
      const newObj = {}
      // 將傳入的物件賦值給該空物件的原型
      Object.setPrototypeOf(newObj, o)
      // 返回該空物件
      return newObj
    }
    
  • 直接使用Object.create()方法,該方法可以建立一個新物件,使用現有的物件來提供新建立的物件的__proto__

    const protoObj = {
      name: 'curry',
      age: 30
    }
    
    const obj = Object.create(protoObj)
    console.log(obj.name) // curry
    console.log(obj.age) // 30
    console.log(obj.__proto__ === protoObj) // true
    

(2)寄生組合式繼承的實現

寄生式繼承就是將物件的原型式繼承和工廠模式進行結合,即封裝一個函式來實現繼承的過程。而這樣結合起來實現的繼承,又可以稱之為寄生組合式繼承。下面就看看具體的實現過程吧。

  • 建立一個原型指向Person父類的物件,將其賦值給Student子類的原型;
  • 在上面的實現方案中,Student子類的例項物件的型別都是Person,可以通過重新定義constructor來優化;
// 定義Person父類公共的屬性
function Person(name, age) {
  this.name = name
  this.age = age
}
// 定義Person父類的公共方法
Person.prototype.say = function() {
  console.log('I am ' + this.name)
}

// 定義Student子類特有的屬性
function Student(name, age, sno) {
  // 通過call呼叫Person父類,建立自己的name和age屬性
  Person.call(this, name, age)
  this.sno = sno
}

// 呼叫Object.create方法生成一個原型指向Person原型的物件,並將這個物件賦值給Student的原型
Student.prototype = Object.create(Person.prototype)
// 定義Student原型上constructor的值為Student
Object.defineProperty(Student.prototype, 'constructor', {
  configurable: true,
  enumerable: false,
  writable: true,
  value: Student
})

// 定義Student子類特有的方法
Student.prototype.studying = function() {
  console.log(this.name + ' studying')
}

// 例項化Student
const stu1 = new Student('curry', 30, 101111)
const stu2 = new Student('kobe', 24, 101112)
console.log(stu1) // Student { name: 'curry', age: 30, sno: 101111 }
console.log(stu2) // Student { name: 'kobe', age: 24, sno: 101112 }

記憶體表現:

總結:

  • 多個地方用到了繼承,可以將上面的核心程式碼賦值在一個函式裡面,如果不想用Object.create(),也可以使用上面封裝的createObj函式;

    function createObj(o) {
      function Fn() {}
      Fn.prototype = o
      return new Fn()
    }
    
    /**
     * @param {function} SubClass 
     * @param {function} SuperClass 
     */
    function inherit(SubClass, SuperClass) {
      SubClass.prototype = createObj(SuperClass.prototype)
      Object.defineProperty(SubClass.prototype, 'constructor', {
        configurable: true,
        enumerable: false,
        writable: true,
        value: SubClass
      })
    }
    
  • 寄生組合式實現繼承的原理其實就是建立一個空物件用於存放子類原型上的方法,並且這個物件的原型指向父類的原型,在ES6中推出的class的實現原理就在這;

相關文章