逐行解讀John Resig對js中單繼承的代碼實現-understanding the simple inheritance of javascript from John Resig

最近重溫了下John Resig經典的js單繼承實現,覺得很有必要記錄下來目前自己對大牛代碼的理解,對自己來說也是一種階段性的總結。

如下是作者的代碼實現(注:方便快速閱讀,已把英文註釋翻譯成了中文,裏面有些註釋是本人aboli加的):

// Simple JavaScript Inheritance
//簡單的JavaScript繼承

/* 
  I’ve been doing a lot of work, lately, with JavaScript inheritance – namely for my work-in-progress 
  JavaScript book – and in doing so have examined a number of different JavaScript classical-inheritance-simulating 
  techniques. Out of all the ones that I’ve looked at I think my favorites were the implementations 
  employed by base2 and Prototype.
  I wanted to go about extracting the soul of these techniques into a simple, re-usable, form that could 
  be easily understood and didn’t have any dependencies. Additionally I wanted the result to be simple 
  and highly usable. Here’s an example of what you can do with it:

  最近,我一直在做很多工作,使用JavaScript繼承 - 即我正在進行的JavaScript書籍 - 並且這樣做已經檢查了許多不同
  的JavaScript經典繼承模擬技術。 在我看過的所有內容中,我認爲我最喜歡的是base2和Prototype所採用的實現。
  我想把這些技術的靈魂提取成一個簡單,可重複使用的形式,這個形式很容易被理解,並且沒有任何依賴性。 另外,
  我希望結果簡單且高度可用。 以下是您可以使用它做的一個示例:
*/

// var Person = Class.extend({
//   init: function(isDancing){
//     this.dancing = isDancing;
//   },
//   dance: function(){
//     return this.dancing;
//   }
// });
 
// var Ninja = Person.extend({
//   init: function(){
//     this._super( false );
//   },
//   dance: function(){
//     // Call the inherited version of dance()
//     return this._super();
//   },
//   swingSword: function(){
//     return true;
//   }
// });
 
// var p = new Person(true);
// p.dance(); // => true
 
// var n = new Ninja();
// n.dance(); // => false
// n.swingSword(); // => true

/*
  Should all be true
  p instanceof Person && p instanceof Class &&
  n instanceof Ninja && n instanceof Person && n instanceof Class
  A couple things to note about this implementation:

  Creating a constructor had to be simple (in this case simply providing an init method does the trick).
  In order to create a new ‘class’ you must extend (sub-class) an existing class.
  All of the ‘classes’ inherit from a single ancestor: Class. Therefore if you want to create a brand 
  new class it must be a sub-class of Class.
  And the most challenging one: Access to overridden methods had to be provided (with their context properly set). 
  You can see this with the use of  this._super(), above, calling the original init() and dance() 
  methods of the  Person super-class.
  I’m pleased with the result: It helps to enforce the notion of ‘classes’ as a structure, maintains 
  simple inheritance, and allows for the super method calling.

  Simple Class Creation and Inheritance

  And here’s the implementation (reasonably sized and commented well) – clocking in at around 25 lines. Feedback is welcome and appreciated.
  
  如下應該都是true:
  p instanceof Person && p instanceof Class &&
  n instanceof Ninja && n instanceof Person && n instanceof Class
  有關此實現的幾點注意事項:

  創建構造函數必須簡單(在這種情況下,簡單地提供init方法就可以了)。
  爲了創建一個新的“類”,您必須擴展(子類)現有的類。
  所有'類'都繼承自單個祖先:Class。因此,如果要創建一個全新的類,它必須是Class的子類。
  最具挑戰性的一個:必須提供對重寫方法的訪問(正確設置其上下文)。您可以通過使用上面的this._super()
  來調用Person超類的原始init()和dance()方法。
  我對結果感到滿意:它有助於強化“類”作爲結構的概念,維護簡單的繼承,並允許超級方法調用。

  簡單的類創建和繼承

  這裏是實現(合理的大小和評論很好) - 大約25行。歡迎並感謝您的反饋
*/

/* Simple JavaScript Inheritance
 * By John Resig https://johnresig.com/
 * MIT Licensed.
 */
// Inspired by base2 and Prototype
(function(){
  var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
 
  // The base Class implementation (does nothing)
  // 基類實現(什麼都不做)
  this.Class = function(){};
   
  // Create a new Class that inherits from this class
  // 創建一個繼承自此類的新類
  Class.extend = function(prop) {
    var _super = this.prototype;
     
    // Instantiate a base class (but only create the instance, don't run the init constructor)
    // 實例化基類(但只創建實例,不要運行init構造函數)
    initializing = true;
    var prototype = new this();
    initializing = false;
     
    // Copy the properties over onto the new prototype
    // 將屬性複製到新原型上
    for (var name in prop) {
      // Check if we're overwriting an existing function
      // 檢查我們是否覆蓋現有功能

      // reviewed by aboli:fnTest.test(prop[name])是檢測屬性值中是否有_super這個單詞,即子類是否需要
      // 調用父類的同名方法
      prototype[name] = typeof prop[name] == "function" && 
        typeof _super[name] == "function" && fnTest.test(prop[name]) ?
        (function(name, fn){
          // reviewed by aboli:這個閉包僅提供了讓子類能夠訪問到其父類的手段,
          // 賦值子類屬性時this是無法確定的,僅當子類調用了該匿名函數時,才能將this確定爲調用該匿名函數的子類
          return function() {
            var tmp = this._super;  // reviewed by aboli:緩存子類中的_super屬性值
             
            // Add a new ._super() method that is the same method but on the super-class
            // 添加一個新的._super()方法,該方法是父類上的同名方法
            this._super = _super[name];   // reviewed by aboli:將屬性值中的_super方法替換爲父類的同名方法
             
            // The method only need to be bound temporarily, so we remove it when we're done executing
            // 該方法只需臨時綁定,所以我們在我們完成執行後刪除它
            var ret = fn.apply(this, arguments);        
            this._super = tmp;
             
            return ret;
          };
        })(name, prop[name]) :
        prop[name];
    }
     
    // The dummy class constructor
    // 虛擬類構造函數
    function Class() {
      // All construction is actually done in the init method
      // 所有構造實際上都是在init方法中完成的
      if ( !initializing && this.init )
        this.init.apply(this, arguments);
    }
     
    // Populate our constructed prototype object
    // 填充我們構建的原型對象
    Class.prototype = prototype;
     
    // Enforce the constructor to be what we expect
    // 強制構造函數符合我們的期望
    Class.prototype.constructor = Class;
 
    // And make this class extendable
    // 並使這個類可擴展
    Class.extend = arguments.callee;
     
    return Class;
  };
})();


/*
  In my opinion the two trickiest parts are the “initializing/don’t call init” and “create _super 
  method” portions. I want to cover those briefly so that you will have a good understanding of 
  what’s being achieved in this method.

  Initialization

  In order to simulate inheritance with a function prototype we use the traditional technique of 
  creating an instance of the super-class function and assigning it to the prototype. Without using 
  the above it would look something like this:

  function Person(){}
  function Ninja(){}
  Ninja.prototype = new Person();
  // Allows for instanceof to work:
  (new Ninja()) instanceof Person
  What’s challenging about this, though, is that all we really want is the benefits of ‘instanceof’, 
  not the whole cost of instantiating a Person object and running its constructor. To counteract this 
  we have a variable in our code, initializing, that is set to true whenever we want to instantiate a 
  class with the sole purpose of using it for a prototype.

  Thus when it comes time to actually construct the function we make sure that we’re not in an 
  initialization mode and run the init method accordingly:

  if ( !initializing )
    this.init.apply(this, arguments);
  What’s especially important about this is that the init method could be running all sorts of costly 
  startup code (connecting to a server, creating DOM elements, who knows) so circumventing this ends 
  up working quite well.

  Super Method

  When you’re doing inheritance, creating a class that inherits functionality from a super-class, a 
  frequent desire is the ability to access a method that you’ve overridden. The final result, in this 
  particular implementation, is a new temporary method (._super) which is only accessible from within 
  a sub-classes’ method, referencing the super-classes’ associated method.

  For example, if you wanted to call a super-classes’ constructor you could do that with this technique.


  在我看來,兩個最棘手的部分是“初始化/不調用init”和“創建_super方法”部分。我想簡要介紹一下這些內容,以便您對
  這種方法所取得的成果有一個很好的理解。

  初始化

  爲了使用函數原型模擬繼承,我們使用傳統技術創建超類函數的實例並將其分配給原型。如果不使用上述內容,它將看起來
  像這樣:

  function Person(){}
  function Ninja(){}
  Ninja.prototype = new Person();
  //允許instanceof工作:
  (new Ninja()) instanceof Person
  但是,對此有什麼挑戰,我們真正想要的只是'instanceof'的好處,而不是實例化Person對象和運行其構造函數的全部成本。
  爲了抵消這種情況,我們在代碼中有一個變量,初始化,只要我們想要實例化一個類,其唯一目的是將它用於原型,就設
  置爲true。

  因此,當實際構建函數時,我們確保我們不處於初始化模式並相應地運行init方法:

  if ( !initializing )
    this.init.apply(this, arguments);
  特別重要的是init方法可以運行各種昂貴的啓動代碼(連接到服務器,創建DOM元素,誰知道),因此規避這一點最終會很好
  地運行。

  超級方法

  在進行繼承時,創建一個從超類繼承功能的類,經常需要能夠訪問您已覆蓋的方法。在這個特定的實現中,最終結果是一個新
  的臨時方法(._super),它只能從子類的方法中訪問,引用超類的相關方法。

  例如,如果您想調用超類的構造函數,則可以使用此技術執行此操作。
*/

var Person = Class.extend({
  init: function(isDancing){
    this.dancing = isDancing;
  }
});
 
var Ninja = Person.extend({
  init: function(){
    this._super( false );
  }
});
 
var p = new Person(true);
p.dancing; // => true
 
var n = new Ninja();
n.dancing; // => false

/*
  Implementing this functionality is a multi-step process. To start, note the object literal that 
  we’re using to extend an existing class (such as the one being passed in to  Person.extend) needs 
  to be merged on to the base new Person instance (the construction of which was described 
  previously). During this merge we do a simple check: Is the property that we’re attempting merge 
  a function and is what we’re replacing also a function? If that’s the case then we need to go 
  about creating a way for our super method to work.

  Note that we create an anonymous closure (which returns a function) that will encapsulate the new 
  super-enhanced method. To start we need to be a good citizen and save a reference to the old 
  this._super (disregarding if it actually exists) and restore it after we’re done. This will help 
  for the case where a variable with the same name already exists (don’t want to accidentally blow 
    it away).

  Next we create the new _super method, which is just a reference to the method that exists on the 
  super-class’ prototype. Thankfully we don’t have to make any additional changes, or re-scoping, 
  here as the context of the function will be set automatically when it’s a property of our object 
  (this will refer to our instance as opposed to the super-class’).

  Finally we call our original method, it does its work (possibly making use of _super as well) 
  after which we restore _super to its original state and return from the function.

  Now there’s a number of ways in which a similar result, to the above, could be achieved (I’ve seen 
    implementations that have bound the super method to the method itself, accessible from 
    arguments.callee) but I feel that this technique provides the best mix of usability and 
    simplicity.

  I’ll be covering a lot more of the nitty-gritty behind the JavaScript prototype system in my 
  completed work but I just wanted to get this Class implementation out there to get everyone 
  trying it out and playing with it. I think there’s a lot to be said for simplistic code 
  (easier to learn, easier to extend, less to download) so I think this implementation is a 
  good place to start and learn the fundamentals of JavaScript class construction and inheritance.

  實現此功能是一個多步驟的過程。首先,請注意我們用於擴展現有類(例如傳遞給Person.extend的類)的對象文字需要
  合併到基本的新Person實例(其結構如前所述) 。在這個合併過程中,我們做了一個簡單的檢查:我們正在嘗試合併一
  個函數的屬性是否正在替換一個函數?如果是這種情況,那麼我們需要爲我們的超級方法創建一種方法。

  請注意,我們創建了一個匿名閉包(返回一個函數),它將封裝新的超級增強方法。首先,我們需要成爲一個好公民並
  保存對舊this._super的引用(忽略它是否確實存在)並在完成後恢復它。這將有助於已存在具有相同名稱的變量的情
  況(不想意外地將其吹掉)。

  接下來,我們創建新的_super方法,它只是對超類原型上存在的方法的引用。值得慶幸的是,我們不必進行任何額外的
  更改或重新確定範圍,因爲當它是我們對象的屬性時,它將自動設置函數的上下文(這將引用我們的實例而不是超類') )。

  最後我們調用我們的原始方法,它完成它的工作(也可能使用_super),之後我們將_super恢復到其原始狀態並從函數
  返回。

  現在有很多方法可以實現上面的類似結果(我已經看到將超級方法綁定到方法本身的實現,可以從arguments.callee訪問)
  但我覺得這種技術提供了可用性和簡單性的最佳組合。

  在我完成的工作中,我將介紹JavaScript原型系統背後的更多細節,但我只是想讓這個類實現在那裏讓每個人都嘗試並使用
  它。我認爲簡化代碼有很多要說的(更容易學習,更容易擴展,更少下載)所以我認爲這個實現是一個很好的起點和學習
  JavaScript類構造和繼承的基礎知識的地方。
*/

逐行解讀下代碼:

從結構上看,整個繼承機制的實現被包在一個自執行匿名函數IIFE(Immediately Invoked Function Expression)中,避免了全局污染,隱藏了實現細節。如下:

(function() {

...

})();

從結果上看,該匿名函數執行完後,會給全局對象window(此處的this指向全局對象)添加一個Class屬性,該屬性指向一個匿名函數,併爲該匿名函數添加了一個extend方法,因爲在js中,一切皆是對象,函數也是對象,extend就是該匿名函數的一個屬性。如下:

(function() {

    var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;

    this.Class = function(){};

    Class.extend = function(prop) {

    ...

    };

})();

另外,對於函數內部定義的兩個變量initializing、fnTest也很有必要講解下。通讀並理解完整個代碼,或許你纔會對這兩個變量的作用有清晰的認知,從而對大牛在代碼功能實現的整體把控和技術手段上由衷讚歎。首先說下initializing變量:

initializing被初始化爲一個布爾值,它用來控制子類中init方法的執行。如果是通過父類調用extend構建的構造函數,則在獲取父類實例時,此時的initializing值爲true,不會執行父類的init方法,起作用的是如下這段:

Class.extend = function(prop) {
    var _super = this.prototype;
    initializing = true;
    var prototype = new this();
    initializing = false;

    ...

    function Class() {
      if ( !initializing && this.init )
        this.init.apply(this, arguments);
    }
};

;如果是實例化構造函數,此時的initializing已經是false,則會執行對應構造函數的init方法,完成初始化工作。也許好多人會對爲什麼實例化構造函數(new的方式)的時候initializing的值是false產生疑問,這裏可以解答下:在John Resig實現的這個單繼承代碼中,構造函數的產生,總是其父類調用自身的extend實現的,extend函數會返回內部的Class函數,這就是子類的構造函數,extend函數實際是一個閉包。由父類調用extend產生的每個子類都記錄了父級作用域的initializing變量,當子類的構造函數在extend方法中返回時,initializing已被重新賦值爲false,所以後續在new一個子類實例的時候,訪問到的initializing總是false,這就保證了只有實例化構造函數纔會執行構造函數的init方法。在js中new一個構造函數的內部過程如下:

function F() {
    ...
}


new F()    ->    function newInstance(F) {
                    return function() {
                        var p = {};
                        p.__proto__ = F.prototype;
                        F.call(p);
                        return p;
                    };
                 }

fnTest被賦值爲一個正則表達式,作用是用來檢測給子類原型對象賦值的屬性中,對應的屬性值中是否有調用_super方法,如果有_super這個單詞,那麼就將該屬性值替換爲一個IIFE,返回一個閉包,該閉包記錄了構建子類構造函數的對象的原型對象,即父類構造函數的原型對象,這句話比較拗口,還是從代碼中解釋:

  Class.extend = function(prop) {
    var _super = this.prototype;
     
    var prototype = new this();

    for (var name in prop) {
      prototype[name] = typeof prop[name] == "function" && 
        typeof _super[name] == "function" && fnTest.test(prop[name]) ?
        (function(name, fn){
          return function() {
            ...
            this._super = _super[name];   
            ...
          };
        })(name, prop[name]) :
        prop[name];
    }
    ...
  };

如上圖代碼,父類通過extend方法構建子類構造函數時,此時的this指向父類構造函數,_super存儲即是父類構造函數的原型對象,prototype保存的是父類實例。所以當new Ninja()調用時,會調用this.init,init方法通過原型鏈在Ninja構造函數的原型對象中找到,即IIFE,內部的_super指向Person.protoype。這樣一來,一切都很清晰。

對於繼承機制,子類構造函數的原型對象被設定爲父類實例,這樣子類既擁有了父類的屬性,又能夠訪問父類原型對象(通過隱藏的__proto__),並且在父類構建子類構造函數時,還會對傳入的對象參數給子類原型對象循環加上自己的屬性跟方法。並且,返回的子類構造函數被添加了extend方法,保證了鏈式調用的能力。

讀完整個實現代碼,還是不由得對John Resig大牛的深厚功底由衷欽佩,閉包,this指向,原型,原型鏈,作用域...這種拿捏自如的手法,還是有好長的路要走~~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章