功能实现参考了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函数,之后判断参数中是否有initialize和greet,并将他们复制到NewClass的prototype中,最后返回。当对返回对象进行new操作时就会调用initialize函数。这就实现了最初代码所展现的功能。
但是,这里传入参数限定了只有initialize或greet才能复制到其原型上,那么我传入的参数不止这两个呢?所以得对代码进行修改,使其通用化,并实现继承功能。
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。