日常好奇-看看ES6的類如何實現的[一]

Xiaowei發表於2018-07-18

為了真正理解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中的的繼承是如何實現的?我們下一篇繼續一起學習

相關文章