为什么要插件化开发
插件化开发可以让我们拥有更高的拓展性和可读性,符合当今流行的“函数化”编程思想,便于多人合作编码降低冲突等。使用插件化开发我们有能力在一个实例被创建后还能够为其增加/删除功能。例如下面的例子:
class A{}
class B{
fuc(){console.log('fuc');}
}
let a = new A();
在日常开发中我们经常会思考,有没有一种方法能够让a可以多出B类中定义的fuc方法?即,使A类的所有已创建和将来会新创建的实例拥有B类中所有功能?即,把B类作为插件附加到A上面?答案是肯定的!
对象原型prototype
创建一个类之后所有的类成员都会保存在其prototype属性中,如下:
class A{
name;
_age;
fuc(){
return 'fuc';
}
get age(){
return this._age;
}
set age(v){
this._age = Number(v);
}
}
let a = new A();
console.log(a);//在控制台中展开输出的a对象,可以看到我们声明的成员全部出现在其__proto__属性中(该属性也可以通过A.prototype属性来获取)
在使用类继承时,其实子类继承的就是父类的prototype:
class AChild extends A{
fuc(){
return super.fuc() + 'Child';
}
}
let ac = new AChild();
console.log(ac);//可以看到在ac.__proto__中还有一个__proto__指向父类的prototype
那么借助这个原理,是否我们可以通过在一个类的prototype上添加成员就可以让所有该类的实例中都能使用这个成员呢?答案是肯定的,下面是一个例子:
A.prototype.log = function(msg){
console.log(msg);
}
a.log('hey!');//正常输出,没有跳出log成员为空的错误
从上例中可以看到,对类的prototype进行修改后,不仅会对接下来新new出来的实例产生影响,对修改前已存在的类的实例也会造成影响。
定义属性的方式
对于一般的成员属性和函数,我们仅需要使用A.prototype.newProprty = 1; A.prototype.newFuc = function(){}
这样的形式就可以了,但是对于getter/setter还有一些更复杂的需求而言这样的方式就不够用了,此时我们需要使用Object.defineProperty(object , propName , descriptor)
方法,该方法中会使用descriptor参数来描述object对象的propName属性,descriptor作为属性描述对象,其中具有大量丰富的配置项可供选择,具体定义可以查询官方文档获悉,下面是定义一个getter/setter的例子:
Object.defineProperty(A.prototype, "age", {
set: function(newValue) {
this._age = Number(newValue);
},
get: function() {
return this._age;
}
});
如果所有属性都通过这种方式来定义那的确有点麻烦了,是否有一种写法可以更简便,更接近我们日常的编码习惯呢?这时你需要使用Object.getOwnPropertyNames(object) 和 Object.getOwnPropertyDescriptor(object , propName)
方法,这两个方法可以分别用于获取一个对象的所有属性名及获取到其propName属性的描述内容。有这个方法的话,我们就有可能使用日常编码习惯写出一个类,然后将该类中所有成员都拷贝到另一个类的prototype上:
【下列代码为了标识变量类别,使用TypeScript语法编写,实际使用时请去掉类型声明】
//此方法取自TypeScript开发手册中的Mixins一章
function applyMixins(copyTo: any, copyFrom: any[]) {
copyFrom.forEach(from => {
Object.getOwnPropertyNames(from.prototype).forEach(name => {
Object.defineProperty(copyTo.prototype, name, Object.getOwnPropertyDescriptor(from.prototype, name));
});
});
}
class To{}
class From{
fuc(){
console.log('fuc');
};
}
applyMixins(To, [From]);//执行这句代码后,所有From类中的成员都会复制到To类中,这就使To具备了From的所有能力
(new To()).fuc();
有了上例中的applyMixins
方法,我们其实已经具备了插件化开发的能力,类To想要那些能力就把那些类作为插件附加进来。不过对于插件来说它不应该把自身的构造函数作为需要重载的内容,一旦覆盖了其载体的构造函数,那么其载体在new的时候得到的就是另一个类的实例了,这明显属于“继承”而不属于“插件”,故需要在遍历插件属性时需要加个判断:
function applyMixins(target: any, scripts: any[]) {
let targetPrototype = target.prototype;
scripts.forEach(baseCtor => {
let baseCtorPrototype = baseCtor.prototype;
Object.getOwnPropertyNames(baseCtorPrototype).forEach(name => {
//排除构造函数
if(name != 'constructor')
{
Object.defineProperty(targetPrototype, name, Object.getOwnPropertyDescriptor(baseCtorPrototype, name));
}
});
});
}
但现在新的一个问题出现了,就是我如何将To类还原到其初始状态,即如何卸载插件呢?
卸载插件
卸载插件的实现思路应当是:
- 附加插件时将原始属性记录下来
- 卸载插件时遍历当前自身属性并与原始属性进行比较,多出的部分删除,其余部分还原
为了实现第一步的记录工作,我们需要先找个地方记录原始属性。由于JS中一切皆对象,而对象又都是可以动态增减属性的,故我们直接在原始对象To中增加一个属性_original作为属性存储位置:
function applyMixins(target: any, scripts: any[]) {
let targetPrototype = target.prototype;
//首次添加插件时需要存下target原始prototype,注意这里存的是属性描述信息
if(!target._original){
target._original = {};
Object.getOwnPropertyNames(targetPrototype).forEach(name => {
target._original[name] = Object.getOwnPropertyDescriptor(targetPrototype, name);
});
}
scripts.forEach(baseCtor => {
......
});
}
function reset(target: any){
let original = target._original;
if(original){
let targetPrototype = target.prototype;
Object.getOwnPropertyNames(targetPrototype).forEach(name => {
if(original[name])
{
Object.defineProperty(targetPrototype, name, original[name]);
}
else
{
delete targetPrototype[name];
}
});
}
}
有了上述代码我们基本上就可以自由地增加/删除插件了:
class To{}
class From{
fuc(){
console.log('fuc');
};
}
applyMixins(To, [From]);
let t = new To();
t.fuc();//输出fuc
reset(To);
console.log(t.fuc);//输出undefined
死循环问题
下列代码中对于super.fuc()的调用会导致死循环的发生:
class A{
fuc(){
return 'A';
}
}
class AChild extends A{
fuc(){
return super.fuc() + 'Child';//此处的super指的是A.prototype
}
}
applyMixins(A, [AChild]);//此时A.prototype.fuc已经变成了AChild.prototype.fuc
(new A()).fuc();//死循环发生
上述死循环发生原因在于A.prototype.fuc变成了AChild.prototype.fuc之后,super.fuc指向的就是其自己。
为了避免死循环的发生,请在使用插件时避免使用super,而改用A._original
来替代,替代之后,为了保证方法中的this指向正确,需要在调用使用使用apply方法:
fuc(){
return A._original.fuc.apply(this) + 'Child';
}
存在的问题
在使用过程中我们会发现,使用applyMixins只能把插件中定义的方法拷贝过来,而不能拷贝成员属性,如下:
class A{}
class AChild extends A{
name='AChild';
fuc(){
return 'Child';
}
}
applyMixins(A, [AChild]);
let a = new A();
console.log(a.name);//输出undefined
如果你调试一下就会发现,在applyMixins中,使用Object.getOwnPropertyNames
方法能获取到的只有function及getter/setter。而且对于插件来说,它从其父类中继承得到的方法也不能被附加到target上面。对于第一个问题,即不能拷贝成员属性,我们可以通过硬性规定必须定义getter/setter来解决,然后在getter中返回默认参数,上例中可改成:
class AChild extends A{
get name(){
return this._name === undefined ? 'AChild' : this._name;
}
set name(v){
this._name = v;
}
}
applyMixins(A, [AChild]);
let a = new A();
console.log(a.name);//输出 'AChild'
对于第二个问题,可以在applyMixins中增加遍历深度,直到target.prototype.__proto__.constructor == Object
为止