天天看點

逐行解讀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指向,原型,原型鍊,作用域...這種拿捏自如的手法,還是有好長的路要走~~~

繼續閱讀