0. 前言
我們帶著問題去閱讀本文:
- 為什麼說ES6的class是語法糖?
- class是原型的語法糖嗎?
- 那又是如何使用原型來實現class這一語法糖的呢?
1. 基於Prototype的OOP
先來看一個prototype的例子:
function Person (name, sex) {
this.name = name
this.sex = sex
}
function Man (name) {
this.name = name
}
Man.prototype = new Person('', 'male')
let Jy = new Man('Jy')
console.log(Jy.name, Jy.sex) // Jy, male
複製程式碼
這是我們使用原型的一個很簡單的例子,Person具有名字和性別,Man是一個性別為男的Person,Jy是一個Man。我們先記住這一個例子,下面將使用class重寫這個例子。
Tips: new, this等是Brendan Eich使之更像Java的OOP而加上的,有興趣的讀者可以自行查閱相關資訊。
2. ES6 Class的OOP
class Person {
constructor (name, sex) {
this.name = name
this.sex = sex
}
}
class Man extends Person {
constructor (name) {
super('', 'male')
this.name = name
}
}
let Jy = new Man('Jy')
console.log(Jy.name, Jy.sex) // Jy, 'male'
複製程式碼
我們通過重寫這個例子,採用了class、constructor、extends、super 這些單詞,接下來就具體來說說ES6規範中對它們做了什麼。
3. 使用Prototype實現的Class OOP(ES6規範)
在ES6之前,JS物件其實就是屬性的集合,而屬性則是一組鍵值對(key, value),key可以是String or Symbol, value包括資料屬性特徵值和訪問器特徵值。
你說普通的屬性還好,不還有物件下面的方法嗎?怎麼就變成了屬性的集合呢?
其實在ES5規範中出現的method的定義是“function that is the value of a property”,是物件的函式屬性而已,不能稱之為方法,直到ES6出現,規範中才有Method Definitions。
我們能想到的在ES3有關OOP的東西: prototype、new、 this、 constructor、 instanceof, 甚至不是規範的 __proto__
屬性。
所幸的是在ES5中我們增加了很多方法來補全它,使之完備:
- Object.defineProperty
- Object.freeze
- Object.create
- Object.getPrototypeOf
- Object.setPrototypeOf
- isPrototypeOf
- ......
再來看一段程式碼:
let obj = {
name: 'Jy',
speak () { // Note: it's not speak: function () {}
console.log(this.name, super.name)
}
}
obj.speak() // Jy, undefined
Object.setPrototypeOf(obj, { name: 'super' })
obj.speak() // Jy, super
let speak = obj.speak
speak() // undefined, super
複製程式碼
obj.speak在ES6中定義已經是Method了,它具有屬性[[homeObject]],homeObject指向方法被呼叫的物件(程式碼中指的是obj), 它是繫結在物件中的Internal Slots,也就是你不能去修改,就相當於寫死了。
那麼homeObject有什麼用呢?它跟super密切相關,當解析到super這一關鍵字的時候就會找homeObject的prototype。
簡單來說,總結為下面兩條公式:
- let homeObj = Method[[HomeObject]] = obj
- super = Object.getPrototypeOf(homeObj)
Note: homeObject是靜態繫結在internal slots中的,而super是動態查詢的。
講完super,我們來講講extends和constructor
class A extends B { }
class A extends B {
constructor (...args) {
super(args)
}
}
class C extends null { }
複製程式碼
extends主要做了以下兩件事:
- Object.setPrototypeOf(A, B)
- Object.setPrototypeOf(A.prototype, B.prototype)
如果父類是null, 則執行Object.setPrototypeOf(C.prototype, null)
上述程式碼的第一和第二部分割槽別在於有沒有顯示宣告constructor, 那麼這兩段程式碼是否等價呢?答案是等價的。
規範中就是這麼定義的:
程式碼的第三部分是繼承了null, 它不會報語法錯誤,但是我們無法new一個C出來,原因是new的時候會呼叫null的constructor,而null沒有constructor。
看到這裡,ES6的class oop, 規範宣告都是使用原型來操作,所以我們是不是可以說class是原型的語法糖了?
4. babel編譯後的class
我們實際專案中多采用babel來編譯ES6、7的程式碼,所以這節我們就來分析以下babel編譯後的程式碼,其中會省略一些報錯、型別檢測的一些相關程式碼來更好地呈現使用原型來實現OOP的主題。
編譯前:
class A extends B {}
console.log(new A)
複製程式碼
編譯後:
"use strict";
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
var A =
/*#__PURE__*/
function (_B) {
_inherits(A, _B);
function A() {
return _getPrototypeOf(A).apply(this, arguments);
}
return A;
}(B);
console.log(new A());
複製程式碼
我們重點看_inherits 方法,跟我們上述說的extends做的兩件事是一樣的:
- Object.setPrototypeOf(subClass, superClass)
- Object.setPrototypeOf(subClass.prototype, superClass.prototype)
只不過它採用的是Object.create方法,這兩個方法的區別可以去MDN上檢視。
再看function A內部,其實就是執行了B的構造器函式來達到super(arguments)的效果, 這個與規範:如果沒有顯示宣告constructor會自動加上constructor是一致的。
5. 總結
至此,我們終於理解了為什麼class是原型的語法糖以及如何使用原型來實現class這一語法糖。
但切記我們使用原型的目的並不是來模擬class oop的,prototype based的oop應該用prototype去理解而不是class。
ES6的class oop 是不完備的 ,例如abstract class 、interface、private等都還沒有,不過有些功能已經在提案中了,大家可以擁抱它,或者TypeScript是個不錯的選擇,如果你的專案中使用到了TS, 歡迎你到評論區分享你的感受。