继承工厂实现-深刻理解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

相关文章
相关标签/搜索