先聊個5毛錢的背景吧
自從有了babel這一個利器之後,es6現在已經被廣泛的使用。JavaScript 類實質上是 JavaScript 現有的基於原型的繼承的語法糖。類語法不會為JavaScript引入新的物件導向的繼承模型。也就是說babel無論是實現類,還是實現繼承,本質上都是基於原型及原型鏈的,那我我們就順這這個思路,一步一步往下走,直到揭開babel是如何實現類及類的繼承的。
原型和原型鏈
javascript中原型的概念比較抽象,不是很好理解,我們還是老老實實上程式碼,用程式碼來舉例說明。
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}
let p1 = new Person('張三', 18, '前端攻城獅')
複製程式碼
我們建立了一個Person的建構函式,並用new 建立了一個該物件的例項物件p1。
- 在js中,每一個函式都有一個prototype屬性。
- 每一個js物件(除null)外都有一個__proto__的屬性。
那麼問題來啦,函式Person的prototype的屬性指向的是什麼呢?是不是Person的原型呢?我們在chrome中輸入如下程式碼
我們發現Person.prototype指向一個物件,實際上這個物件就是p1的原型。如圖: 因此,我們也可以用下面的這個圖來表示他們之間的關係 - 前面我們說過,Person.prototype指向一個物件,那麼這個物件中都有什麼呢?我們再在chrome中輸入如下程式碼如圖: 我們發現Person.prototype的確是一個物件,這個物件裡面有兩個屬性:constrcutor, 和__proto__這兩個屬性。constructor指向Person(),也說明js物件果真都有一個__proto__的屬性啊。那問題來啦constructor到底和Person是什麼關係呢?實際上,constructor屬性指向的就是建構函式本身。有圖有真相
- 每個原型上都有一個constructor的屬性指向關聯的建構函式。他們的關係如下: 前面我們發現Person.prototype也有__proto__屬性,那Person.prototype.__proto__是什麼呢?換言之,原型的原型是什麼呢。先來一張圖 我們發現它只有constructor屬性,該屬性指向Object(),沒有__proto__屬性。是不是很有意思。那我們再做個實驗吧。如圖
- 原型物件就是通過Object建構函式生成的,例項的__proto__指向建構函式的prototype,Object建構函式的原型的__proto__指向null
- 我們用一張圖總結一下吧: 實際上這個就是原型鏈啦。
ES5中的繼承
前面我們聊了聊原型和原型鏈,其實就是為了我們聊繼承做鋪墊的。廢話不多說,我們上程式碼吧:
// 我們定義一個動物類,裡面有一個型別的屬性
function Animal (type) {
this.type = type
}
// 動物都有吃東西的方法
Animal.prototype.eat = function () {
console.log('我在吃東西')
}
// 我們來個小貓咪類吧
function Cat () {}
// 現在我們要實現讓Cat繼承Animal的屬性和方法,該怎麼做呢?
複製程式碼
- 第一種方法
// 我們將cat類的原型直接指向Animal的例項
Cat.prototype = new Animal()
let cat = new Cat()
console.log(cat.eat())
複製程式碼
結果如圖
我們發現無法繼承父類的屬性。我們改造一下,藉助call方法,程式碼入下:function Animal (type) {
this.type = type
}
Animal.prototype.eat = function () {
console.log('我在吃東西')
}
function Cat (type) {
Animal.call(this, type)
}
Cat.prototype = new Animal()
let cat = new Cat('喵咪')
console.log(cat.eat())
console.log(cat.type)
複製程式碼
執行結果如下:
nice!!!似乎完美的解決了問題,但真的是這樣麼? 我就不賣關子啦,實際上是有問題的,有什麼問題呢?我們上程式碼:function Animal (type, val) {
this.type = type
this.sum = [4,5,6] // 隨便給的屬性啊,為了說明問題。
this.setSum = function () {
this.sum.push(val)
}
}
Animal.prototype.eat = function () {
console.log('我在吃東西')
}
function Cat (type, val) {
Animal.call(this, type, val)
}
Cat.prototype = new Animal()
let cat = new Cat('喵咪', 1)
let cat2 = new Cat('貓咪2', 2)
console.log(cat.setSum())
console.log(cat2.setSum())
console.log(cat.sum)
console.log(cat2.sum)
複製程式碼
執行結果如圖:
發現了沒有,圖中setSum方法和sum屬性都是父類定義的,但是子類可以呼叫。所以這個不是我們想要的結果;還有一點就是Cat的constructor屬性,此時指向的並不是Cat而是Animal。那應該怎麼解決呢?- 第二種方法: 這個時候我們就不得不拿出我們的終極神器啦。它就是ES5的Object.create()方法。程式碼如下:
function inherit(C, P) {
// 等同於臨時建構函式
C.prototype = Object.create(P.prototype);
C.prototype.constructor = C; // 修復constructor
C.super = P;//儲存超類
}
function Animal(type) {
this.type = type;
}
Animal.prototype.eat = function () {
console.log('動物都是要吃飯滴')
}
function Cat(type, talk) {
Cat.super.call(this, type)
this.talk = talk
this.getTalk = function () {
return this.talk
}
}
inherit(Cat, Animal)
let cat = new Cat('貓咪', '我們一起學貓叫')
console.log(cat.getTalk())
複製程式碼
程式碼執行如下:
這樣我們就比較完美的解決了繼承的問題。babel對es6類的實現
在背景中我們已經聊到了es6中類的繼承實際上是一個語法糖,現在我們就想辦法撥開這顆糖。
- es6類重寫上面程式碼
class Animal {
constructor (type) {
this.type = type
}
eat () {
console.log('動物都要吃飯')
}
}
class Cat extends Animal {
constructor (type,talk) {
super(type) // 繼承父類constructor的屬性
this.talk = talk
}
getTalk () {
return this.talk
}
}
let cat = new Cat('喵咪', '喵喵喵')
console.log(cat.getTalk()) // 喵喵喵
console.log(cat.type) // 喵咪
複製程式碼
- babel中類的實現
babel中類的實現主要是三個步驟:- 步驟一:
建構函式的實現,這部分的實現與我們普通的建構函式的區別就是,通過一個自執行函式包裹,程式碼如下
var Animal = function () { function Animal (type) { this.type = type this.getType = function () { return this.type } } return Animal }() var Cat = function () { function Cat (talk) { this.talk = talk this.getTalk = function () { return this.talk } } return Cat }() 複製程式碼
- 步驟二:
如下程式碼:
因此babel中對此做了校驗,這就是第二步要乾的事情。程式碼如下:function Animal() { return { name: '牛逼', age: 18 } } let animal = new Animal() console.log(animal) // { name: '牛逼', age: 18} 複製程式碼
var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("不能返回一個物件") } } // 嗯,你沒有看錯就是這麼幾行程式碼 var Animal = function () { function Animal (type) { _classCallCheck(this, Animal) this.type = type } }() ...此處省略喵咪類 複製程式碼
- 步驟三:
原型上方法的新增。前面兩步主要是對建構函式的實現及校驗,步驟三就是把原型上的方法新增到對應的類上。這裡我們主要要聊聊babel是如何對 es6中類的方法的處理,也就是
如上面程式碼中的eat方法和sleep方法的處理。那怎麼能把方法新增到一個物件上呢?沒錯,Object.defineProperty()這個方法就是用來幹這件事情的,babel中也是用他來處理的。回到我們的話題,在babel中,它是通過_createClass這個函式來處理的。廢話不多說我們上程式碼:class Animal { eat () { console.log('aaa') } sleep () { console.log('bbb') } } 複製程式碼
/* _createClass()這個函式接受兩個引數, 一個是這個類的建構函式,即給哪個類新增方法。 第二個引數是一個陣列物件,類似這樣嬸的 [{key: 'eat', val: function eat () { console.log('aaa') }}] 是不是恍然大悟?那接下來我們就實現一下吧 */ function definePropties (target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor) } } var _createClass = function () { return function (Constructor, protoProps, staticProps) { // 新增原型上的方法 if (protoProps) definePropties(Constructor.prototype, protoProps) // 新增靜態的方法 if (staticProps) definePropties(Constructor, staticProps) return Constructor } }() _createClass(Animal,[{ key: 'eat', value: function () { console.log('aaa') } }]) 複製程式碼
- 步驟一:
建構函式的實現,這部分的實現與我們普通的建構函式的區別就是,通過一個自執行函式包裹,程式碼如下
babel中繼承的實現
接下來,終於到了我們的大boss啦,上面我們聊了聊babel中是怎麼處理es6中類的,這節我們就聊聊babel中是怎麼處理es6中的繼承。
- babel中繼承的實現 babel中繼承的實現的思路基本跟我們前面聊的es5的繼承思路基本一致。下面我們來聊一聊。
function _inherits(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true;
}
})
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
複製程式碼
好啦,這個話題就聊到這裡,歡迎大家拍磚啊。