繼承工廠實現-深刻理解JS原型鏈繼承原理

功能實現參考了Leaflet源碼。ios

功能介紹

咱們構造一個Class類,實現如下功能:git

  1. 基礎的繼承功能、提供了初始化函數。
  2. 初始函數鉤子(hook)功能。
  3. 內置選項的繼承與合併。
  4. 靜態屬性方法。
  5. Mixins。

基礎繼承

JavaScript的繼承

JavaScirpt並非一個典型的OOP語言,因此其繼承實現略爲繁瑣,是基於原型鏈的實現,但好在ES6實現了Class的語法糖,能夠方便的進行繼承。github

Leaflet可能爲了瀏覽器的兼容,因此並未採用ES6的語法,同時也大量使用了[polyfill]的寫法(在[Util.js]中實現)。關於polyfill,之後進行專門介紹。數組

繼承實現

在leaflet中,咱們能夠這樣寫:瀏覽器

let Parent = Class.extend({
  initialize(name) { //初始函數
    this.name = name;
  },
  greet() {
    console.log('hello ' + this.name);
  }
});

let parent = new Parent('whj');
parent.greet(); // hello whj

使用L.Class.extend接收一個對象參數建立了Parent的構造函數,以後實例化調用greet函數輸出hello whjapp

實際上L.Class.extend返回了一個函數(JavaScript是以函數實現類的功能)。函數

如下是實現代碼:測試

function Class() {} // 聲明一個函數Class

Class.extend = function (props) { // 靜態方法extend
  var NewClass = function () {
    if (this.initialize) {
      this.initialize.apply(this, arguments);//由於並不知道initialize
    }                       //傳入參數數量,因此使用apply
  } 

  if (props.initialize){
    NewClass.prototype.initialize = props.initialize;
  }

  if (props.greet) {
    NewClass.prototype.greet  = props.greet;
  }

 return NewClass;
};

能夠看見Class的靜態方法extend中,聲明瞭一個NewClass函數,以後判斷參數中是否有initializegreet,並將他們複製到NewClassprototype中,最後返回。當對返回對象進行new操做時就會調用initialize函數。這就實現了最初代碼所展示的功能。this

可是,這裏傳入參數限定了只有initializegreet才能複製到其原型上,那麼我傳入的參數不止這兩個呢?因此得對代碼進行修改,使其通用化,並實現繼承功能。prototype

Class.extend = function (props) {
  var NewClass = function () {
    if (this.initialize) {
      this.initialize.apply(this, arguments);
    }
  }

 //將父類的prototype取出並複製到NewClass的__super__ 靜態變量中
  var parentProto = NewClass.__super__ = this.prototype;
  var proto = Object.create(parentProto); //複製parentProto到proto中
                       //protos是一個新的prototype對象
  proto.constructor = NewClass; 
  NewClass.prototype = proto; //到這完成繼承

  extend(proto, props); //將參數複製到NewClass的prototypez中

  return NewClass;
};

將父類的原型prototype取出,Object.create函數返回了一個全新的父類原型prototype對象proto,將其構造函數指向當前NewClass,最後將其賦給NewClass的原型,至此完成了繼承工做。注意,此時NewClass只是繼承了Class
完成繼承操做以後調用extend函數將props參數複製到NewClass的原型proto上。

extend函數實現以下:

function extend(dest) {
  var i, j, len, src;

  for (j = 1, len = arguments.length; j < len; j++) {
    src = arguments[j];
    for (i in src) {
      dest[i] = src[i];
    }
  }
  return dest;
}

須要注意的是arguments的用法,這是一個內置變量,保存着傳入的全部參數,是一個類數組結構。

如今離實現繼承只差一步了 (•̀ᴗ•́)و ̑̑ 。

function Class() { }

Class.extend = function (props) {
  var NewClass = function () {
    ...
  }
    ...
 for (var i in this) {
  if (this.hasOwnProperty(i) && i !== 'prototype' && i !== '__super__') {
    NewClass[i] = this[i];
    }
  }
  ...
  return NewClass;
};

for循環中將父類的靜態方法(不在原型鏈上的、非prototype、非super)複製到NewClass中。

如今,基本的繼承已經實現。 <(▰˘◡˘▰)>

測試代碼:

let Parent = Class.extend({
  initialize(name) {
    this.name = name;
  },
  greet(word) {
    console.log(word + this.name);
  }
});

let Child = Parent.extend({
  initialize(name,age) {
    Parent.prototype.initialize.call(this,name);
    this.age = age;
  },
  greet() {
    Parent.prototype.greet.call(this,this.age);
  }
});

let child = new Child('whj',22);
child.greet(); //22whj

初始函數鉤子

這個功能能夠在已存在的類中添加新的初始化函數,其子類也繼承了這個函數。

let Parent = Class.extend({
  initialize(name) {
    this.name = name;
  },
  greet(word) {
    console.log(word + this.name);
  }
});  // 類已構造完成

Parent.addInitHook(function () { //新增init函數
  console.log("Parent's other init");
});

let parent = new Parent(); // Parent's other init

能夠看見類實例化時執行了新增的init函數。

爲了完成這個功能咱們在代碼上進行進一步修改。

首先在Class上新增addInitHook這個方法:

Class.addInitHook = function (fn) {
  var init = fn;

  this.prototype._initHooks = this.prototype._initHooks || [];
  this.prototype._initHooks.push(init);
  return this;
};

將新增函數push進_initHooks_initHooks中的函數以後會被依次調用。

Class.extend = function (props) {
  var NewClass = function () {
    if (this.initialize) {
      this.initialize.apply(this, arguments);
    }
    this.callInitHooks(); // 執行調用新增的init函數的函數
  }

  ...

  proto._initHooks = []; // 新增的init函數數組

  proto.callInitHooks = function () {
    ...
  };

  return NewClass;
};

首先在原型上新增一個保存着初始化函數的數組 _initHooks、調用新增初始函數的方法
callInitHooks,最後在NewClass中調用callInitHooks

如今看下callInitHooks的實現:

proto.callInitHooks = function () {
    if (this._initHooksCalled) { // 是新增函數否已被調用
      return;
    }

    if (parentProto.callInitHooks) { //先調用父類的新增函數
      parentProto.callInitHooks.call(this);
    }

    this._initHooksCalled = true; // 此init已被調用,標誌位置爲true

    for (var i = 0, len = proto._initHooks.length; i < len; i++) {
      proto._initHooks[i].call(this); // 循環調用新增的初始化函數
    }
  };

執行這段函數時,先會遞歸的調用父類的callInitHooks函數,以後循環調用已構建好的
_initHooks數組中的初始函數。

內置選項

首先看下示例程序:

var Parent= Class.extend({
    options: {
        myOption1: 'foo',
        myOption2: 'bar'
    }
});

var Child = Parent.extend({
    options: {
        myOption1: 'baz',
        myOption3: 5
    }
});

var child = new Child ();
child.options.myOption1; // 'baz'
child.options.myOption2; // 'bar'
child.options.myOption3; // 5

在父類與子類中都聲明瞭options選項,子類繼承其options並覆蓋了父類同名的options

實現以下:

Class.extend = function (props) {
  var NewClass = function () {
    ...
  }
  ...
  if (proto.options) {
     props.options = extend(proto.options, props.options);
  }
  ...
  return NewClass;
};

這個功能有了以前的基礎實現就至關簡單了。判斷父類是否有optios選項,如有者將子類的optios進行復制。

靜態屬性方法

var MyClass = Class.extend({
  statics: {
      FOO: 'bar',
      BLA: 5
  }
});

MyClass.FOO; // 'bar'

實現以下:

Class.extend = function (props) {
  var NewClass = function () {
    ...
  }
  ...
  if (props.statics) {
     extend(NewClass, props.statics);
     delete props.statics;
  }
  ...

  extend(proto, props);

  ...
  return NewClass;
};

實現與內置選項相似,需注意的是extend執行以後得把props中的statics字段刪除,以避免以後重複複製到原型上。

Mixins

Mixins 是一個在舊類上添加新的屬性、方法的技術。

var MyMixin = {
    foo: function () { console.log('foo') },
    bar: 5
};

var MyClass = Class.extend({
    includes: MyMixin
});

// or 
// MyClass.include(MyMixin);

var a = new MyClass();
a.foo(); // foo

實現與靜態屬性方法相似:

Class.extend = function (props) {
  var NewClass = function () {
    ...
  }
  ...
  if (props.includes) {
     extend.apply(null, [proto].concat(props.includes));
     delete props.includes;
  }
  extend(proto, props); //將參數複製到NewClass的prototypez中
  
  return NewClass;
};

Class.include = function (props) {
   Util.extend(this.prototype, props);
   return this;
};

也是一樣調用了extend函數,將include複製到原型中。爲何使用apply方法,主要是爲了支持include爲數組的狀況。

總結

Leaflet中繼承功能已所有實現完成。實現思路與一些小技巧值得咱們借鑑。

這是完整實現代碼

文章首發於Whj's Website

相關文章
相關標籤/搜索