上篇咱們講解了構造函數和原型等前端概念的原理,知道了實例之間如何經過構造函數的prototype來共享方法,下篇咱們主要看下用es6的class怎麼來實現,以及class的繼承等。同上篇同樣,重點在於背後的原理,只有懂得了爲何要這麼設計,咱們才能真正的說【精通】。javascript
假以下篇你看得很辛苦,那說明你對原型的掌握還不夠啊喂,請務必先熟讀理解上篇。前端
一次性精通javascript原型/繼承/構造函數/類的原理(上)java
回顧下以前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)
時:編程
zac
constructor
方法自動運行一次,同時把傳參'zac', 28
賦值給新對象因此這個class究竟是什麼呢?其實class就是個函數而已。segmentfault
console.log(typeof User) // function
那麼class背後究竟是怎麼運做的呢?閉包
完事,看到了嗎?javascript的class只是構造函數的語法糖而已(固然class還作了一些其餘的小工做)函數
es6爲咱們引進了類class的概念,看起來更接近其餘面向對象編程的語言了,但不一樣於其餘oop語言的類繼承,javascript的繼承仍然是經過原型來實現的。oop
咱們接下來看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
提供給咱們的。主要有兩個用法:
super(...)
是用來調用父類的constructor方法(只能在constructor裏這麼調用)super.method(...)
是用來調用父類的方法咱們如今分開來看這兩點,先解決第一個問題:爲何要在子類的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有一個本質區別:
因此,子類必須在本身的construtor裏調用super()
來讓它的父類去執行父類的constructor,不然this
就不會被建立,而後如上例所示咱們就獲得一個error。
順便說一句,如今你應該能理解爲何咱們在寫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,所以:
this.__proto__.sing()
,調用者是this.__proto__,至關於admin.__proto__, 也就是user對象,因此最後打印出來的是:User is singing.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
運行上面的代碼,立刻就報錯了,報錯告訴咱們超過了最大調用棧的範圍,這個錯通常說明咱們的代碼出現裏無限循環調用。咱們再來逐層解析:
superAdmin.sing()
,因此運行第(**)
句時,this=superAdmin,所以:this.__proto__.sing.call(this) //(**) //至關於 superAdmin.__proto__.sing.call(this) //至關於 admin.sing.call(this) //至關於咱們去執行admin裏的sing方法時,this仍然是superAdmin
(*)
句,這時候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語法的:
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')
若是你完整的吸取理解了上下篇的內容,上面的代碼應該很好理解了吧?我仍是帶着你們再來解析一次:
(*)
句,咱們但願Admin構造函數可以擁有User構造函數的屬性name和age,因此咱們用當前的上下文this去執行一遍User構造函數(**)
句,咱們將Admin.prototype設置爲以User.prototype爲原型的新對象。這裏你可能有疑問爲何要用Object.create,而不是直接把User.prototype賦值給Admin?緣由就是咱們不但願Admin和User共用同一個prototype啊,這是咱們爲何要使用繼承的初衷啊(***)
句作的事好,寫到這裏我以爲差很少了,咱們從建立一個對象講起,因爲想要批量建立對象咱們講到了構造函數,又由於對象之間想要共享方法從而講到了原型,最後瓜熟蒂落講到了對象的繼承。整個脈絡應該是比較清晰的。
這是《前端原理系列》的初篇,下一篇按計劃應該是講調用棧/執行上下文/閉包/事件循環機制這個主題,你們記得關注,下期再會。