继承工厂实现-深入理解JS原型链继承原理

功能实现参考了Leaflet源码。

功能介绍

我们构造一个Class类,实现以下功能:

  1. 基础的继承功能、提供了初始化函数。
  2. 初始函数钩子(hook)功能。
  3. 内置选项的继承与合并。
  4. 静态属性方法。
  5. Mixins。

基础继承

JavaScript的继承

JavaScirpt并不是一个典型的OOP语言,所以其继承实现略为繁琐,是基于原型链的实现,但好在ES6实现了Class的语法糖,可以方便的进行继承。

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 whj

实际上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函数。这就实现了最初代码所展现的功能。

但是,这里传入参数限定了只有initializegreet才能复制到其原型上,那么我传入的参数不止这两个呢?所以得对代码进行修改,使其通用化,并实现继承功能。

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。

你可能感兴趣的:(javascript,leaflet,原型链)