詳解JS類概念的實現

衆所周知,JS並無類(class)的概念,雖說ES6開始有了類的概念,可是,這並非說JS有了像Ruby、Java這些基於類的面嚮對象語言同樣,有了全新的繼承模型。ES6中的類,僅僅只是基於現有的原型繼承的一種語法糖,下面咱們好好分析一下,具體是如何實現的javascript

面向對象思想

在講正題以前,咱們先來討論一下各類面試題均可能出現的一個問題,什麼是面向對象編程(OOP)?java

  • 類:定義某一事物的抽象特色,包含屬性和方法,舉個栗子,這個類包含狗的一些基礎特徵,如毛皮顏色,吠叫等能力。面試

  • 對象:類的一個實例,仍是舉個栗子,小明家的白色的狗和小紅家紅色的狗。express

  • 屬性:對象的特徵,好比剛提到的狗皮毛的顏色。編程

  • 方法:對象的行爲,好比剛纔提到的狗的吠叫能力。babel

  • 封裝性:經過限制只有特定類的對象能夠訪問特定類的成員,通常包含public protected private 三種,不一樣語言的實現不一樣。閉包

  • 繼承性:一個類會有子類,這個子類是更具體化的一個抽象,它包含父類的一些屬性和方法,而且有可能有不一樣於父類的屬性和方法。函數

  • 多態性:多意爲‘許多’,態意爲‘形態’。不一樣類能夠定義相同的方法或屬性。this

  • 抽象性:複雜現實問題轉化爲類定義的途徑,包括以上全部內容。spa

如何實現對象(類)的定義

因爲JS並無類(class)的概念,更多的時候咱們把它叫作對象(function),而後把對象叫作實例(instance),跟團隊裏面的人討論OOP的時候,常常會有概念上的一些誤解,特此說明一下。

構造函數:一個指明瞭對象類型的函數,一般咱們能夠經過構造函數類建立

在js裏面,咱們一般都是經過構造函數來建立對象(class),而後經過new這個關鍵字來實例化一個對象,如:

function Dog(name){
  this.name = name;
}
var d1 = new Dog("dodo");
d1.constructor
// Dog(name){
//  this.name = name;
// }

var d2 = new Dog('do2do');

爲何經過構造函數能夠實現對象(class)屬性的定義呢?首先,咱們必須理解這個語法new constructor[([arguments])]

咱們來具體看看當new Dog('name')時,具體作了哪些事情

  1. 一個新實例被建立。它繼承自Dog.prototype

  2. 構造函數被執行,相應的參數會被傳入,同時上下文(this)會指向這個新的實例

  3. 除非明確返回值,不然返回新的實例

至此,咱們實現了OOP裏面的類(Dog)、對象(d1,d2)、和屬性(name)的概念,d1d2有相同的name屬性,可是值並不相同,即屬性是私有的。

注: 新建立的實例,都包含一個constructor屬性,該屬性指向他們的構造函數Dog

原型對象(prototype)

接下來,咱們即將討論如何定義方法,其實,咱們徹底能夠這樣定義咱們的方法,如:

function Dog(name){
  this.name = name;
  this.bark = function(){
    console.log(this.name + " bark");
  };
}
var d1 = new Dog("dodo");
d1.bark();
// dodo bark

可是,通常咱們不推薦這麼作,正如咱們所知Dog是一個構造函數,每次實例化時,都會執行這個函數,也就是說,bark 這個方法每次都會被定義, 比較浪費內存。可是咱們一般能夠用constructor和閉包的方式來實現私有屬性,如:

function Dog(name){
  this.name = name;
  
  // barkCount 是私有屬性,由於實例並不知道這個屬性
  var barkCount = 0;
  this.bark = function(){
    barkCount ++;
    console.log(this.name + " bark");
  };
  this.getBarkCount = function(){
    console.log(this.name + " has barked " + barkCount + " times");
  };
}
var d1 = new Dog("dodo");
d1.bark();
d1.bark();
d1.getBarkCount();
// dodo has barked 2 times

好像扯得有點遠,咱們迴歸咱們的主角prototype,函數Dog有一個特殊的屬性,這個屬性就叫原型,如上所述,當用new運算符建立實例時,會把Dog的原型對象的引用複製到新的實例內部的[[Prototype]]屬性,即d1.[[Prototype]] = Dog.prototype,由於全部的實例的[[Prototype]]都指向Dog的原型對象,那麼,咱們就能夠很方便的定義咱們的方法了,如:

function Dog(name){
  this.name = name;
}

Dog.prototype = {
  bark: function(){
    console.log(this.name + " bark");
  }
};

var d1 = new Dog("dodo");
d1.bark();
// dodo bark

咱們能夠經過d1.__proto__ == Dog.prototype,來驗證咱們的想法。用原型對象還有一個好處,因爲實例化的對象的[[Prototype]]指向Dog的原型對象,那麼咱們能夠經過添加Dog的原型對象的方法,來添加已經實例化後的實例d1的方法。如:

Dog.prototype.run = function(){
  console.log(this.name + " is running!");
}
d1.run();
// dodo is running!

注:全部對象的__proto__都指向其構造器的prototype

原型鏈

上面已經描述如何定義一個,接下來咱們將要了解,如何實現類的繼承。在此以前,咱們先了解js裏一個老生常談的概念:原型鏈:每一個對象都有一個指向它的原型(prototype)對象的內部連接。這個原型對象又有本身的原型,直到某個對象的原型爲 null 爲止(也就是再也不有原型指向),組成這條鏈的最後一環。這種一級一級的鏈結構就稱爲原型鏈

mozilla給出一個挺好的例子:

// 假定有一個對象 o, 其自身的屬性(own properties)有 a 和 b:
// {a: 1, b: 2}
// o 的原型 o.[[Prototype]]有屬性 b 和 c:
// {b: 3, c: 4}
// 最後, o.[[Prototype]].[[Prototype]] 是 null.
// 這就是原型鏈的末尾,即 null,
// 根據定義,null 沒有[[Prototype]].
// 綜上,整個原型鏈以下: 
// {a:1, b:2} ---> {b:3, c:4} ---> null

console.log(o.a); // 1
// a是o的自身屬性嗎?是的,該屬性的值爲1

console.log(o.b); // 2
// b是o的自身屬性嗎?是的,該屬性的值爲2
// o.[[Prototype]]上還有一個'b'屬性,可是它不會被訪問到.這種狀況稱爲"屬性遮蔽 (property shadowing)".

console.log(o.c); // 4
// c是o的自身屬性嗎?不是,那看看o.[[Prototype]]上有沒有.
// c是o.[[Prototype]]的自身屬性嗎?是的,該屬性的值爲4

console.log(o.d); // undefined
// d是o的自身屬性嗎?不是,那看看o.[[Prototype]]上有沒有.
// d是o.[[Prototype]]的自身屬性嗎?不是,那看看o.[[Prototype]].[[Prototype]]上有沒有.
// o.[[Prototype]].[[Prototype]]爲null,中止搜索,
// 沒有d屬性,返回undefined

如今咱們能夠經過咱們理解的構造函數和原型對象來實現繼承的概念了,代碼以下:

function Dog(name){
  this.name = name;
}

// 這種寫法會修改dog實例的constructor,能夠經過Dog.prototype.constructor = Dog來重置
Dog.prototype = {
  bark: function(){
    console.log(this.name + " bark");
  }
};

// 重置Dog實例的構造函數爲自己
Dog.prototype.constructor = Dog;

// Haski 的構造函數
function Haski(name){
  // 繼承Dog的構造函數
  Dog.call(this, name);
  // 能夠補充更多Haski的屬性
  this.type = "Haski";
};

// 1. 設置Haski的prototype爲Dog的實例對象
// 2. 此時Haski的原型鏈是 Haski -> Dog的實例 -> Dog -> Object
// 3. 此時,Haski包含了Dog的全部屬性和方法,並且還有一個指針,指向Dog的原型對象
// 4. 這種作法是不推薦的,下面會改進
Haski.prototype = new Dog();

// 重置Haski實例的構造函數爲自己
Haski.prototype.constructor = Haski;

// 能夠爲子類添加更多的方法
Haski.prototype.say = function(){
  console.log("I'm " + this.name);
}

var ha = new Haski("Ha");
// Ha bark
ha.bark();
// Ha bark
ha.say();
// I'm Ha

注: 子類在定義prototype時,不可直接使用Haski.prototype = {}定義,這樣會重寫Haski的原型鏈,把Haski的原型當作Object的實例,而非Dog的實例

可是,當我想找一下ha的原型鏈時,會發現ha的原型對象指向的是Dog的實例,並且還有一個值爲undefinedname屬性,在實例化時,name是不必的, 以下圖:

因此,咱們須要修改一下咱們的實現,代碼以下:

// 修改前
Haski.prototype = new Dog();

// 修改後
Haski.prototype = Object.create(Dog.prototype);

注: __proto__ 方法已棄用,從 ECMAScript 6 開始, [[Prototype]] 能夠用Object.getPrototypeOf()和Object.setPrototypeOf()訪問器來訪問

自此,咱們已經實現繼承的概念,父類有本身的方法,子類繼承了父類的屬性和方法,並且還能夠定義本身的屬性和方法。

ES6 如何實現

'use strict';
// 聲明 Dog 類
class Dog {
  // 構造函數
  constructor(name){
    this.name = name;
  }
 
  // 普通方法
  dark(){
    console.log(this.name + "bark");
  }
 
  // 靜態方法,也叫類方法
  static staticMethod(){
    console.log("I'm static method!");
  }
}

// 經過`extends`關鍵字來實現繼承
class Haski extends Dog {
  constructor(name){
    // 調用父類的構造函數
    super(name);
    this.type = "Haski";
  }
  
  // 定義子類方法
  say(){
    console.log("I'm" + this.name);
  }
}

在ES6中,咱們只需經過class extends super constructor 便可比較方便的完成原來使用JS比較難理解的實現,咱們能夠經過babel的解析器,來看看babel是怎麼把這些語法糖轉成JS的實現的。具體代碼能夠參考

"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 _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

// 聲明 Dog 類

var Dog = function () {
  // 構造函數

  function Dog(name) {
    _classCallCheck(this, Dog);

    this.name = name;
  }

  // 普通方法


  _createClass(Dog, [{
    key: "dark",
    value: function dark() {
      console.log(this.name + "bark");
    }

    // 靜態方法,也叫類方法

  }], [{
    key: "staticMethod",
    value: function staticMethod() {
      console.log("I'm static method!");
    }
  }]);

  return Dog;
}();

// 經過`extends`關鍵字來實現繼承


var Haski = function (_Dog) {
  _inherits(Haski, _Dog);

  function Haski(name) {
    _classCallCheck(this, Haski);

    var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Haski).call(this, name));
    // 調用父類的構造函數


    _this.type = "Haski";
    return _this;
  }

  _createClass(Haski, [{
    key: "say",
    value: function say() {
      console.log("I'm" + this.name);
    }
  }]);

  return Haski;
}(Dog);

教是最好的學,我正在嘗試把我本身理解的內容分享出來,但願我能講清楚,若是描述有誤,歡迎指正。

參考文獻

相關文章
相關標籤/搜索