es5 編寫類風格的代碼

分享下《JavaScript忍者祕籍》中的一種編寫類風格代碼的方法

JavaScript可讓咱們經過原型實現繼承,許多開發人員,尤爲是那些有傳統面向對象背景的開發人員,都但願將JavaScript的繼承系統簡化並抽象成一個他們更熟悉的系統。
因此,這不可避免地引導咱們走向類的領域。類是面向對象開發人員所指望的內容,儘管JavaScript自己不支持傳統的類繼承。
一般,這些開發人員但願它有以下特性:javascript

  • 一套能夠構建新構造器函數和原型的輕量級系統
  • 一種簡單的方式來執行原型繼承
  • 一種能夠訪問被函數原型所覆蓋的方法的途徑

如下代碼展現了一個能夠實現上述目標的示例。

//經過subClass()方法,建立一個Person類做爲Object的一個子類,該方法以後實現
var Person = Object.subClass({
  init: function (isDancing) {
    this.dancing = isDancing;
  },
  dance: function () {
    return this.dancing;
  }
});

//經過繼承Person類,建立一個Ninja子類
var Ninja = Person.subClass({
  init: function () {
//須要一種調用父類構造器的方法——這裏展現咱們將這樣作
    this._super(false);
  },
  dance: function () {
    //Ninja-specific stuff here
    return this._super();
  },
  swingSword: function () {
    return true;
  }
});

//建立一個實例對Person類進行測試,看其是否可以跳舞
var person = new Person(true);
assert(person.dance(),
  "The person is dancing.");

//建立一個實例對Ninja類進行測試,看其是否有swingSword方法以及繼承過來的dance方法
var ninja = new Ninja();
assert(ninja.swingSword(),
  "The sword is swinging.");
assert(!ninja.dance(),
  "The ninja is not dancing.");

//執行instanceof測試,驗證類的繼承
assert(person instanceof Person,
  "Person is a Person");
assert(ninja instanceof Ninja && ninja instanceof Person,
  "Ninja is a Ninja and a Person");複製代碼

注意事項:java

  • 經過調用現有構造器函數的subClass()方法能夠建立一個新「類」,例如,經過Object建立一個Person類,以及經過Person建立一個Ninja類
  • 爲了讓構造器的建立更加簡單。咱們建議的語法是,爲每一個類只提供一個init()方法,就像爲Person和Ninja提供的init()方法同樣
  • 咱們全部的「類」最終都繼承於一個祖先:Object。所以,若是要建立一個新類,它必須是Object的一個子類,或者是一個在層級上繼承於Object的類(徹底模仿當前的原型系統)
  • 該語法的最大挑戰是訪問被覆蓋的方法,並且有時這些方法的上下文也有可能被修改了。經過this._super()調用Person的原始init()和dance()方法,咱們就能夠了解這種用法

實現:

(function () {
  var initializing = false,
//粗糙的正則表達式用於判斷函數是否能夠被序列化。
    superPattern =
      /xyz/.test(function () {
        xyz;
      })?
         /\b_super\b/: 
         /.*/;

//給Object添加一個subClass方法
  Object.subClass = function (properties) {
    var _super = this.prototype;

//初始化超類
    initializing = true;
    var proto = new this();
    initializing = false;

    for (var name in properties) {

//將屬性複製到prototype裏
      proto[name] = typeof properties[name] === 'function' &&
      typeof _super[name] === 'function' &&
      superPattern.test(properties[name]) ?
        //定義一個重載函數
        (function (name, fn) {
          return function () {
            var tmp = this._super;

            this._super = _super[name];

            var ret = fn.apply(this, arguments);
            this._super = tmp;

            return ret;
          }
        })(name, properties[name]) :
        properties[name];
    }

//創造一個仿真類構造器
    function Class() {
      if (!initializing && this.init) {
        this.init.apply(this, arguments);
      }
    }

//設置類的原型
    Class.prototype = proto;

//重載構造器引用
    Class.constructor = Class;

//讓類繼續可擴展
    Class.subClass = arguments.callee;

    return Class;
  };
})();複製代碼

檢測函數是否可序列化

代碼實現的一開始就很深奧,並且還可能讓人困惑。在後續代碼中,咱們須要知道瀏覽器是否支持函數序列化。但該測試又有至關複雜的語法,因此如今就要獲得結果,而後保存結果,以便在後續代碼中再也不進行復雜的操做,由於後續代碼自己已經夠複雜了。
函數序列化就是簡單接收一個函數,而後返回該函數的源碼文本。稍後,咱們可使用這種方法檢查一個函數在咱們感興趣的對象中是否存在引用。
在大多數瀏覽器中,函數的toString()方法都會奏效。通常來講 ,一個函數在其上下文中序列化成字符串,會致使它的toString()方法被調用。因此,能夠用這種方法測試函數是否能夠序列化。
在設置一個名爲initializing的變量爲false以後,咱們使用以下表達式測試一個函數是否可以被序列化:正則表達式

/xyz/.test(function () { xyz; })複製代碼

該表達式建立一個包含xyz的函數,將該函數傳遞給正則表達式的test()方法,該正則表達式對字符串「xyz」進行測試。若是函數可以正常序列化(test()方法將接收一個字符串,而後將觸發函數的toString()方法),最終結果將返回true。
使用該文本表達式,咱們在隨後的代碼中使用了該正則表達式:瀏覽器

superPattern =
  /xyz/.test(function () {
    xyz;
  }) ?
    /\b_super\b/ :
    /.*/;複製代碼

創建了一個名爲superPattern的變量,稍後用它來判斷一個函數是否包含字符串"_super"。只有函數支持序列化才能進行判斷,因此在不支持序列化的瀏覽器上,咱們使用一個匹配任意字符串的模式進行代替。服務器

子類的實例化

此時,咱們準備開始定義一個方法用於子類化父類,咱們使用以下代碼進行實現:app

Object.subClass = function (properties) {
  var _super = this.prototype;複製代碼

給Object添加一個subClass()方法,該方法接收一個參數,該參數是咱們指望添加到子類的屬性集。
爲了用函數原型模擬繼承,咱們建立父類的一個實例,並將其賦值給子類的原型。咱們在代碼中定義了一個initializing變量,每當咱們想使用原型實例化一個類的時候,都將該變量設置爲true。
所以,在構造實例時,咱們能夠確保再也不實例化模式下進行構建實例,並能夠相應地運行或跳過init()方法:函數

if (!initializing && this.init) {
  this.init.apply(this, arguments);
}複製代碼

尤爲重要的是,init()方法能夠運行各類昂貴的啓動代碼(鏈接到服務器、建立DOM元素,還有其餘未知內容),因此若是隻是建立一個實例做爲原型的話,咱們要避免任何沒必要要的昂貴啓動代碼。測試

保留父級方法

大多數支持繼承的語言中,在一個方法被覆蓋時,咱們保留了訪問被覆蓋方法的能力。這是頗有用的,由於有時候咱們是想徹底替換方法的功能,但有時候咱們卻只是想增長它。在咱們特定的實現中,咱們建立一個名爲_super的臨時新方法,該方法只能從子類方法內部進行訪問,而且該方法引用的是父類中的原有方法。
例如:ui

var Person = Object.subClass({
    init: function (isDancing) {
        this.dancing = isDancing;
    }
});

var Ninja = Person.subClass({
     init: function () {
        this._super(false);
    }
});複製代碼

在Ninja構造器內,咱們調用了Person的構造器,並傳入了一個相應的值。這能夠防止從新複製代碼——咱們能夠重用父類中已經編寫好的代碼。
該功能的實現是一個多步驟的過程。爲了加強子類,咱們向subClass()方法傳入了一個對象哈希,只須要將父類的屬性和傳入的屬性合併在一塊兒就能夠了。
首先,使用以下代碼,建立一個超類的實例做爲一個原型:this

initializing = true;
var proto = new this();
initializing = false;複製代碼

注意,咱們是如何「保護」初始化代碼的,正如咱們在前一節中討論的initializing變量的值。
如今,是時候將傳入的屬性合併到proto對象中了。若是不在乎父類函數,合併代碼將很是簡單:

for (var name in properties)  {
    proto[name] = properties[name];
}複製代碼

可是,咱們須要關心父類的函數,因此前面的代碼和除了調用父類函數的函數以外是等價的。重寫函數時,能夠經過_super調用父函數,咱們須要經過名爲_super的屬性,將子類函數和父類函數的引用進行包裝。但在完成該操做以前,咱們須要檢測即將被包裝的子類函數。可使用以下條件表達式:

typeof properties[name] === "function" &&
typeof _super[name] === "function" &&
superPattern.test( properties[name] )複製代碼

這個表達式包含三個檢測條件:

  • 子類屬性是不是一個函數?
  • 超類屬性是不是一個函數?
  • 子類函數是否都包含一個_super()引用?

只有三個條件都爲true的時候,咱們才能作所要作的事情,而不是複製屬性值。注意,咱們使用了以前設置的正則表達式,和函數序列化一塊兒,測試函數是否會調用等效的父類。
若是條件表達式代表咱們必須包裝功能,咱們經過給即時函數的結果進行賦值,將該結果做爲子類的屬性:

(function (name, fn) {
  return function () {
    var tmp = this._super;

    this._super = _super[name];

    var ret = fn.apply(this, arguments);
    this._super = tmp;

    return ret;
  }
})(name, properties[name])複製代碼

該即時函數建立並返回了一個新函數,該新函數包裝並執行了子類的函數,同時能夠經過_super屬性訪問父函數。首先須要先保持舊的this._super引用(無論它是否存在),而後處理完之後再恢復該引用。這在同名變量已經存在的狀況下會頗有用(不想意外的丟失它)。接下來,建立新的_super方法,它只是在父類原型中已經存在的一個方法的引用。值得慶幸的是,咱們不須要作任何額外的代碼修改或做用域修改。當函數成爲咱們對象的一個屬性時,該函數的上下文會自動設置(this引用的是當前的子類實例,而不是父類實例)。最後,調用原始的子類方法執行本身的工做(也有可能使用了_super),而後將_super恢復成原來的狀態,並將方法調用結果進行返回。有不少方式能夠達到相似的結果(有的實現,會經過訪問arguments.callee,將_super方法綁定到方法自身),可是該特定技術提供了良好的可用性和簡便性。

相關文章
相關標籤/搜索