為了真正理解ES6中類的概念,來學習類是如何實現的
我們都知道在JS中,函式是“一等公民”,“類”的概念是在ES6中提出的,它好像跟我們自己寫的函式構造器一樣,但又有好像有些不一樣的地方,那麼它到底是如何實現的那?為了達到這個目的,我們利用babel來看下它編譯後的程式碼。
不帶繼承的類
首先我們寫一個簡單的類,該類沒有任何繼承,只有一個簡單的建構函式和getName
函式
class App {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
複製程式碼
然後babel一下,我們得到以下結果:
"use strict";
var _createClass = function () {
function defineProperties(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);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var App = function () {
function App(name) {
_classCallCheck(this, App);
this.name = name;
}
_createClass(App, [{
key: "getName",
value: function getName() {
return name;
}
}]);
return App;
}();
複製程式碼
東西還挺多,一眼並看不出來什麼東西來,我們接下來一點點分析。我們先看最後一個函式:
// 立即執行函式
var App = function () {
// 建構函式變形成這樣
function App(name) {
// 從這個函式的名字上看,好像是類呼叫檢查,我們暫時先不看這個函式
_classCallCheck(this, App);
this.name = name;
}
// 呼叫了一個_createClass函式,應該是在給App附加一些值
_createClass(App, [{
key: "getName",
value: function getName() {
return name;
}
}]);
// 返回一個名為App的函式
return App;
}();
複製程式碼
下面來看_createClass
函式,該函式用來定義各個屬性值:
// 從返回值看,該函式是一個高階函式
var _createClass = function () {
// 為目標值新增多個屬性
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
// 開始設定描述符物件
var descriptor = props[i];
// 預設不可列舉
descriptor.enumerable = descriptor.enumerable || false;
// 預設可配置
descriptor.configurable = true;
// 存在value值則預設可寫
if ("value" in descriptor) descriptor.writable = true;
// 使用Object.defineProperty來設定屬性
Object.defineProperty(target, descriptor.key, descriptor);
}
}
// 函式接收三個引數,分別是:建構函式,原型屬性,靜態屬性
return function (Constructor, protoProps, staticProps) {
// 為建構函式prototype新增屬性(即為用建構函式生成的例項原型新增屬性,可以被例項通過原型鏈訪問到)
if (protoProps) defineProperties(Constructor.prototype, protoProps);
// 為建構函式新增屬性
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
複製程式碼
好像很簡單,跟我們平時使用函式實現差別不是很多,就相差了一個描述符的設定過程。最後看一下類呼叫檢查函式_classCallCheck
:
// 類呼叫檢查,不能像普通函式一樣呼叫,需要使用new關鍵字
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
複製程式碼
增加了錯誤處理,當我們呼叫方式不正確時,丟擲錯誤。
模擬實踐
我們簡單實現以下沒有繼承的方式,來加深我們的印象,為了簡化不新增錯誤處理和描述符的設定過程。
var App = function(name) {
this.name = name;
}
App.prototype.getName = function() {
return this.name;
}
var app = new App('miniapp');
console.log(app.getName()); // 輸出miniapp
複製程式碼
這個很簡單,就是我們平常模擬“類”所使用的方法,js所有物件都是通過原型鏈的方式“克隆”來的。注意我們這裡的App
不能叫做類,在js中沒有類的概念。它是一個函式構造器,它可以被當做普通函式呼叫,也可以被當做函式構造器呼叫,呼叫函式構造器使用new
關鍵字,函式構造器會克隆它的prototype
物件,然後進行一些其他操作,如賦值操作,最後返回一個物件。
下面想一個問題,實現繼承我們一般都是利用原型鏈的方式,像下面這樣:
var dog = {
name: 'goudan'
};
var animal = {
getName: function() {
return this.name;
}
}
// 物件的原型通過`__proto__`暴露出來(tip: 實際中不要這麼寫)
dog.__proto__ = animal;
console.log(dog.getName()); // 輸出goudan
複製程式碼
我們如何在兩個類之間繼承那?在ES6中實現很簡單
class Animal {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Dog extends Animal{
constructor(name) {
super(name);
this.name = name;
}
}
複製程式碼
如果我們自己實現一個要怎麼實現,我們先寫一個:
var Animal = function(name) {
this.name = name;
}
Animal.prototype.getName = function() {
return this.name;
}
var Dog = function(name) {
Animal.call(this, name);
this.name = name;
}
Dog.prototype = Animal.prototype;
var dog = new Dog('goudan');
console.log(dog.getName()); // 輸出goudan
複製程式碼
但這種方式總感覺不太好,那麼ES6中的的繼承是如何實現的?我們下一篇繼續一起學習