一次性精通javascript原型/繼承/構造函數/類的原理(下)

上篇咱們講解了構造函數和原型等前端概念的原理,知道了實例之間如何經過構造函數的prototype來共享方法,下篇咱們主要看下用es6的class怎麼來實現,以及class的繼承等。同上篇同樣,重點在於背後的原理,只有懂得了爲何要這麼設計,咱們才能真正的說【精通】。javascript

假以下篇你看得很辛苦,那說明你對原型的掌握還不夠啊喂,請務必先熟讀理解上篇。前端

一次性精通javascript原型/繼承/構造函數/類的原理(上)java

ES6的class

回顧下以前es5的寫法:react

function User(name, age) {
    this.name = name
    this.age = age
}

User.prototype.grow = function(years) {
    this.age += years
    console.log(`${this.name} is now ${this.age}`)
}
User.prototype.sing = function(song) {
    console.log(`${this.name} is now singing ${song}`)
}

const zac = new User('zac', 28)

es6的寫法:es6

class User {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    grow(years) {
        this.age += years
        console.log(`${this.name} is now ${this.age}`)
    }
    sing(song) {
        console.log(`${this.name} is now singing ${song}`)
    }
}

const zac = new User('zac', 28)

當咱們調用new User('zac', 28)時:編程

  1. 建立了一個新對象zac
  2. constructor方法自動運行一次,同時把傳參'zac', 28賦值給新對象

因此這個class究竟是什麼呢?其實class就是個函數而已。segmentfault

console.log(typeof User) // function

那麼class背後究竟是怎麼運做的呢?閉包

  1. 首先建立了一個叫作User的函數
  2. 而後把class的constructor裏面的代碼原封不動的放到User函數裏
  3. 最後將class的方法,如grow,sing放到User.prototype裏

完事,看到了嗎?javascript的class只是構造函數的語法糖而已(固然class還作了一些其餘的小工做)函數

es6爲咱們引進了類class的概念,看起來更接近其餘面向對象編程的語言了,但不一樣於其餘oop語言的類繼承,javascript的繼承仍然是經過原型來實現的。oop

ES6的extends

咱們接下來看class之間的繼承要怎麼實現,es6爲咱們提供了一個extends的方法:

class User {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    grow(years) {
        this.age += years
        console.log(`${this.name} is now ${this.age}`)
    }
    sing(song) {
        console.log(`${this.name} is now singing ${song}`)
    }
}

class Admin extends User {
    constructor(name, age, address) {
        super(name, age) //to call a parent constructor
        this.address = address
    }
    grow(years) {
        super.grow(years) // to call a parent method
        console.log(`he is admin, he lives in ${this.address}`)
    }
}

const zac = new User('zac', 28)

這裏咱們重點看下兩次super的調用,首先要明確的是super關鍵詞是class提供給咱們的。主要有兩個用法:

  1. super(...)是用來調用父類的constructor方法(只能在constructor裏這麼調用)
  2. super.method(...)是用來調用父類的方法

覆寫父類的constructor

咱們如今分開來看這兩點,先解決第一個問題:爲何要在子類的constructor裏調用一下super()?緣由很簡單,由於javascript規定了:經過繼承(extends)來而的class,必須在constructor裏調用super(),不然在constructor裏調用this會報錯!

不信你看:

class Admin extends User {
    constructor(name, age, address) {
        this.name = name
        this.age = age
        this.address = address
    }
    ...
}

const zac = new admin('zac', 28, 'China') 
// VM1569:3 Uncaught ReferenceError: 
// Must call super constructor in derived class before accessing 'this' or returning from derived constructor

簡單的解釋下,javascript爲何這麼設計:
由於當使用new來實例化class時,直接建立的class和經過extends來建立的class有一個本質區別:

  1. 前者會先建立一個空對象,而後把這個空對象賦值給this
  2. 後者則直接不作這件事,由於它只須要等本身的父類作這個事就好了

因此,子類必須在本身的construtor裏調用super()來讓它的父類去執行父類的constructor,不然this就不會被建立,而後如上例所示咱們就獲得一個error。

在react裏爲何要寫super(props)?

順便說一句,如今你應該能理解爲何咱們在寫react組件時,爲何要寫這句super(props)了吧?

class Checkbox extends React.Component {
  constructor(props) {
    // 如今你還沒法使用this
    super(props);
    // 如今你可使用this啦
    this.state = { isOn: true };
  }
  // ...
}

固然這裏還有有個小問題,假如我不傳props會怎樣super()

// React內部的代碼
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

// 咱們本身代碼
class Checkbox extends React.Component {
  constructor(props) {
    super(); 
    console.log(this.props); // undefined   
  // 
}

這個不難理解吧?你不把props傳給父組件,天然沒法在constructor裏調用this.props咯。但其實你在其餘地方仍是能夠正常調用this.props的,由於react幫咱們多作了一件事:

// React內部的代碼
  const instance = new YourComponent(props);
  instance.props = props;

覆寫父類的方法

相對來講,super.method()就好理解多了。咱們的父類User裏有一個grow的方法,咱們的子類Admin也想有這個方法,同時可能還想在這個方法上再加點別的操做。因此呢,它就先經過class提供的super來先調用一遍父類的grow方法,而後再添加本身的邏輯。

這裏有一些愛思考的同窗可能就會想了,爲何我能夠經過super來調用父類的方法呢?爲何我能夠寫super.method()呢?若是你在考慮這個問題,說明你真的很愛思考,你很棒!

簡單來理解,既然super.method()是調用父類的方法,而咱們的子類又是經過繼承父類而來的,結合以前講過的原型的知識,那super.method()是否是就應該至關於this.__proto__.method呢?直觀上來講確實應該如此,咱們作個簡單的實驗來看下:

let user = {
  name: "User",
  sing() {
    console.log(`${this.name} is singing.`)
  }
}

let admin = {
  __proto__: user,
  name: "Admin",
  sing() {
    this.__proto__.sing.call(this) //(*)
    console.log('calling from admin')
  }
}

admin.sing(); // Admin is singing. calling from admin

能夠看到,user對象是admin對象的原型,主要看下(*)這句話,咱們在當前對象的上下文(this)裏調用了原型對象user的sing方法。注意我用了.call(this),若是沒有這個的話,咱們執行this.__proto__.sing()時是在原型對象user的上下文裏執行的,因此執行this.name時this指向的是user對象:

...
let admin = {
  __proto__: user,
  name: "Admin",
  sing() {
    this.__proto__.sing()
    console.log('calling from admin')
  }
}

admin.sing(); // User is singing. calling from admin

這裏順便解釋下this,敲黑板了,無論你是在對象裏仍是原型了發現了this,它永遠是點(.)左邊的那個對象。假如是user.sing()那this是(.)左邊的user;假如admin.sing()那this就是(.)左邊的admin。

而後咱們再看下上面的例子,咱們調用的方法是admin.sing(),因此運行admin中的sing方法時,this就是admin,所以:

  1. 假如是this.__proto__.sing(),調用者是this.__proto__,至關於admin.__proto__, 也就是user對象,因此最後打印出來的是:User is singing.
  2. 假如是this.__proto__.sing.call(this),這時候咱們經過call手動將調用者改成admin了,因此最後打印出來是:Admin is singing.

好了,有點扯遠了,咱們再回來。剛剛的例子好像確實證實了super.method()至關於this.__proto__.method,咱們再看下面的代碼:

let user = {
  name: "User",
  sing() {
    console.log(`${this.name} is singing.`)
  }
}

let admin = {
  __proto__: user,
  name: "Admin",
  sing() {
    this.__proto__.sing.call(this) //(*)
    console.log('calling from admin')
  }
}

let superAdmin = {
  __proto__: admin,
  name: "SuperAdmin",
  sing() {
    this.__proto__.sing.call(this) //(**)
    console.log('calling from superAdmin')
  }
}

superAdmin.sing(); // VM1900:12 Uncaught RangeError: Maximum call stack size exceeded

運行上面的代碼,立刻就報錯了,報錯告訴咱們超過了最大調用棧的範圍,這個錯通常說明咱們的代碼出現裏無限循環調用。咱們再來逐層解析:

  1. 首先咱們來看調用方法是:superAdmin.sing(),因此運行第(**)句時,this=superAdmin,所以:
this.__proto__.sing.call(this) //(**)
//至關於
superAdmin.__proto__.sing.call(this)
//至關於
admin.sing.call(this) //至關於咱們去執行admin裏的sing方法時,this仍然是superAdmin
  1. 而後就運行到了第(*)句,這時候this=superAdmin,所以:
this.__proto__.sing.call(this) //(*)
//至關於
superAdmin.__proto__.sing.call(this)
//至關於
admin.sing.call(this) //又回到了這裏

而後,結局你就知道,admin.sing不斷循環地調用者本身。因此啊,單純的經過this是沒法解決這個問題的。javascript爲了解決這個問題設計了一個新的內部屬性[[HomeObject]],每當一個函數被指定爲一個對象的方法時,這個方法就有了一個屬性[[HomeObject]],這個屬性固定的指向這個對象:

let user = {
  name: "User",
  sing() { 
    console.log(`${this.name} is singing.`)
  }
}

//admin.sing.[[HomeObject]] == admin
let admin = {
  __proto__: user,
  name: "Admin",
  sing() {   
    super.sing()
    console.log('calling from admin')
  } 
}

// admin.sing.[[HomeObject]] == admin
let superAdmin = {
  __proto__: admin,
  name: "SuperAdmin",
  sing() {
    super.sing() 
    console.log('calling from superAdmin')
  }
} 

superAdmin.sing()
// SuperAdmin is singing.
// calling from admin 
// calling from superAdmin

ok,當咱們運行superAdmin.sing()時,也就是執行super.sing(),每當super關鍵詞出現,javascript引擎就會去找當前方法的[[HomeObject]]對象,而後去找這個對象的原型,最後在這個原型上調用相應的方法。

因此當咱們調用superAdmin.sing()時,至關於執行:

const currentHomeObject = this.sing.[[HomeObject]]
const currentPrototype = Object.getPrototypeOf(currentHomeObject)
currentPrototype.sing.call(this)

ES5如何實現extends

下面咱們對比的來看下,es5是怎麼實現extends語法的:

function User(name, age) {
    this.name = name
    this.age = age
}

User.prototype.grow = function(years) {
    this.age += years
    console.log(`${this.name} is now ${this.age}`)
}
User.prototype.sing = function(song) {
    console.log(`${this.name} is now singing ${song}`)
}

function Admin(name, age, address) {
    User.call(this, name, age) //(*)
    this.address = address
}

Admin.prototype = Object.create(User.prototype) //(**)
Admin.prototype.grow = function(years) {
        User.prototype.grow.call(this, years)
        console.log(`he is admin, he lives in ${this.address}`)
}
Admin.prototype.constructor = Admin //(***)



const zac = new Admin('zac', 28, 'China')

若是你完整的吸取理解了上下篇的內容,上面的代碼應該很好理解了吧?我仍是帶着你們再來解析一次:

  1. (*)句,咱們但願Admin構造函數可以擁有User構造函數的屬性name和age,因此咱們用當前的上下文this去執行一遍User構造函數
  2. 如今咱們在Admin實例化出來的對象裏能夠找到name、age和address屬性了,但咱們還沒法使用grow方法,由於grow方法是定義在User.prototype上的,因此第(**)句,咱們將Admin.prototype設置爲以User.prototype爲原型的新對象。這裏你可能有疑問爲何要用Object.create,而不是直接把User.prototype賦值給Admin?緣由就是咱們不但願Admin和User共用同一個prototype啊,這是咱們爲何要使用繼承的初衷啊
  3. 上篇咱們講過,每一個函數都有一個prototype對象,這個對象裏有一個construtor對象指向這個函數自己。而此時,咱們的Admin構造函數的prototype因爲是直接從User構造函數的prototype繼承來的,因此Admin.prototype.constructor === User.prototype.constructor,所以咱們須要手動的修正下Admin.prototype.constructor,將它指向構造函數Admin自己。這也就是第(***)句作的事

總結

好,寫到這裏我以爲差很少了,咱們從建立一個對象講起,因爲想要批量建立對象咱們講到了構造函數,又由於對象之間想要共享方法從而講到了原型,最後瓜熟蒂落講到了對象的繼承。整個脈絡應該是比較清晰的。

這是《前端原理系列》的初篇,下一篇按計劃應該是講調用棧/執行上下文/閉包/事件循環機制這個主題,你們記得關注,下期再會。

相關文章
相關標籤/搜索