资讯专栏INFORMATION COLUMN

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

Tony / 750人阅读

摘要:继承实现在中,我们可以这样写初始函数使用接收一个对象参数创建了的构造函数,之后实例化调用函数输出。完成继承操作之后调用函数将参数复制到的原型上。

功能实现参考了Leaflet源码。

功能介绍

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

基础的继承功能、提供了初始化函数。

初始函数钩子(hook)功能。

内置选项的继承与合并。

静态属性方法。

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。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88585.html

相关文章

  • 前端进击的巨人(七):走进面向对象,原型原型继承方式

    摘要:除了以上介绍的几种对象创建方式,此外还有寄生构造函数模式稳妥构造函数模式。 showImg(https://segmentfault.com/img/remote/1460000018196128); 面向对象 是以 对象 为中心的编程思想,它的思维方式是构造。 面向对象 编程的三大特点:封装、继承、多态: 封装:属性方法的抽象 继承:一个类继承(复制)另一个类的属性/方法 多态:方...

    wums 评论0 收藏0
  • JavaScript面向对象的程序设计

    摘要:目录导语理解对象和面向对象的程序设计创建对象的方式的继承机制原型对象原型链与原型对象相关的方法小结导语前面的系列文章,基本把的核心知识点的基本语法标准库等章节讲解完本章开始进入核心知识点的高级部分面向对象的程序设计,这一部分的内容将会对对象 目录 导语 1.理解对象和面向对象的程序设计 2.创建对象的方式 3.JavaScript的继承机制 3.1 原型对象 3.2 原型链 3.3 与...

    gitmilk 评论0 收藏0
  • 面向对象的 JavaScript

    摘要:是完全的面向对象语言,它们通过类的形式组织函数和变量,使之不能脱离对象存在。而在基于原型的面向对象方式中,对象则是依靠构造器利用原型构造出来的。 JavaScript 函数式脚本语言特性以及其看似随意的编写风格,导致长期以来人们对这一门语言的误解,即认为 JavaScript 不是一门面向对象的语言,或者只是部分具备一些面向对象的特征。本文将回归面向对象本意,从对语言感悟的角度阐述为什...

    novo 评论0 收藏0
  • 深入理解JS的面向对象(更新中)

    摘要:的面向对象主要包含了两块创建对象继承。构造函数一般来说,我们可以这样定义构造函数构造函数的函数名常大写在这里,我们没有显示的创建对象,没有语句,却将属性和方法赋值给了。 面向对象是软件开发方法。面向对象的概念和应用已超越了程序设计和软件开发,扩展到如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD技术、人工智能等领域。面向对象是一种对现实世界理解和抽象的方法...

    陈伟 评论0 收藏0
  • js对象详解(JavaScript对象深度剖析,深度理解js对象)

    摘要:对象详解对象深度剖析,深度理解对象这算是酝酿很久的一篇文章了。用空构造函数设置类名每个对象都共享相同属性每个对象共享一个方法版本,省内存。 js对象详解(JavaScript对象深度剖析,深度理解js对象) 这算是酝酿很久的一篇文章了。 JavaScript作为一个基于对象(没有类的概念)的语言,从入门到精通到放弃一直会被对象这个问题围绕。 平时发的文章基本都是开发中遇到的问题和对...

    CatalpaFlat 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<