ES6提供了完整的class语法,因此,可以非常方便的使用extends关键字对类进行扩展(继承)。为了实现类的一些基础功能,我撰写了下面这个类,用以被其他类基础,拥有这个基础类的基础功能:
var events = {} var data = {} var copyProperty = function(Target, Source) { for(let key of Reflect.ownKeys(Source)) { if(key !== 'constructor' && key !== 'prototype' && key !== 'name') { let descriptor = Object.getOwnPropertyDescriptor(Source, key) Object.defineProperty(Target, key, descriptor) } } } export default class ClassBase { constructor(...args) { events[this] = {} data[this] = {} this.call(this.initialize, ...args) return this } /** * @desc initialize class method, be called every time class is initialized * Notice: never use constructor when extends by a sub class */ initialize() {} /** * @desc get data from data manager * @param string key: the key of data, you can use '.' to get tree info. e.g. .get('root.sub.ClassBaseMix') => .get('root').sub.ClassBaseMix */ get(key) { let target = data[this] if(key.indexOf('.') === -1) return target[key] let nodes = key.split('.').filter(item => item && item !== '') if(nodes.length === 0) return for(let node of nodes) { if(typeof target !== 'object' || !target[node]) return target = target[node] } return target } /** * @desc save data to data manager * @param string key: the key of data, use '.' to set tree structure. e.g. .set('root.sub.ClassBaseMix', 'value') => .get('root').sub.ClassBaseMix = 'value' * @param mix value: the value to save * @param boolean notify: whether to trigger data change event */ set(key, value, notify = true) { if(!data[this]) data[this] = {} let target = data[this] if(key.indexOf('.') === -1) { target[key] = value if(notify) { this.trigger('change:' + key, value) } return this } let nodes = key.split('.').filter(item => item && item !== '') if(nodes.length === 0) return let lastKey = nodes.pop() for(let node of nodes) { if(typeof target !== 'object') return if(!target[node]) { target[node] = {} } target = target[node] } target[lastKey] = value if(notify) { nodes.push(lastKey) let event = nodes.shift() this.trigger('change:' + event, value) while (nodes.length > 0) { event += '.' + nodes.shift() this.trigger('change:' + event, value) } } return this } /** * @desc call some function out of this class bind with this * @param function factory: the function to call * @param args: arguments to pass to function be called */ call(factory, ...args) { factory.apply(this, args) return this } /** * @desc bind events on Instantiate objects * @param string evts: events want to bind, use ' ' to split different events, e.g. .on('change:data change:name', ...) * @param function handler: function to call back when event triggered * @param number order: the order to call function. functions are listed one by one with using order. */ on(evts, handler, order = 10) { if(!events[this]) events[this] = {} evts = evts.split(' ') let target = events[this] evts.forEach(evt => { if(!target[evt]) { target[evt] = {} } let node = target[evt] if(!node[order]) node[order] = [] let hdles = node[order] if(hdles.indexOf(handler) === -1) hdles.push(handler) // make sure only once in one order }) return this } /** * @desc remove event handlers * @param string event: event name, only one event supported * @param function handler: the function wanted to remove, notice: if you passed it twice, all of them will be removed. If you do not pass handler, all handlers of this event will be removed. */ off(event, handler) { if(!handler) { events[this][event] = {} return } let node = events[this][event] if(!node) return let orders = Object.keys(node) if(!orders || orders.length === 0) return if(orders.length > 1) orders = orders.sort((a, b) => a - b) orders.forEach(order => { let hdles = node[order] let index = hdles.indexOf(handler) if(index > -1) hdles.splice(index, 1) // delete it/them if(hdles.length === 0) delete node[order] }) return this } /** * @desc trigger events handlers * @param string event: which event to trigger * @param args: arguments to pass to handler function */ trigger(event, ...args) { let node = events[this][event] if(!node) return let orders = Object.keys(node) if(!orders || orders.length === 0) return if(orders.length > 1) orders = orders.sort((a, b) => a - b) let handlers = [] orders.forEach(order => { let hdles = node[order] handlers = [...handlers, ...hdles] }) handlers.forEach(handler => { if(typeof handler === 'function') { // this.call(handler, ...args) // 会绑定this handler(...args) // 不会绑定this,其实可以在on的时候用bind去绑定 } }) return this } /** * @desc mix this class with other classes, this class property will never be overwrite, the final output class contains certain property and all of this class's property * @param Classes: the classes passed to mix, previous class will NOT be overwrite by the behind ones. */ static mixin(...Classes) { class ClassBaseMix {} Classes.reverse() Classes.push(this) for(let Mixin of Classes) { copyProperty(ClassBaseMix, Mixin) copyProperty(ClassBaseMix.prototype, Mixin.prototype) } return ClassBaseMix } /** * @desc mix other classes into this class, property may be overwrite by passed class, behind class will cover previous class */ static mixto(...Classes) { class ClassBaseMix {} Classes.unshift(this) for(let Mixin of Classes) { copyProperty(ClassBaseMix, Mixin) copyProperty(ClassBaseMix.prototype, Mixin.prototype) } return ClassBaseMix } toString() { return this.constructor.name } }
你可以在这里阅读每一个方法的说明,这里简单的说明一下它们的各自用途。
initialize方法
用来替代constructor作为实例化方法,虽然在class中使用constructor并没有什么问题,但是大家似乎约定熟成的使用initialize方法替换它。所以constructor方法作为一个最起始的方法,不应该在子类中出现被覆盖,因为这里会用它来调用initialize方法,一旦被覆盖,子类中就不能自动调用initialize方法了。
setter和getter
用以获取和设置私有属性,当然也可以保存其他任何数据类型。这些数据都会被保存在attributions这个变量里面,但是它仅在这个文档中可见,所以不会被外部访问,只有通过get和set方法才能访问。
而且功能有所提升,传入的变量支持用点隔开来表示父子关系。比如set('book.name', 'News')
,这样可以直接设置book对象的name属性,用get('book').name = 'News'
也可以达到这个效果(性能更高),但形式上没有前者好看,使用get('book.name')
这种写法也更优雅。
on、off和trigger
和大多数事件绑定和触发一样,这三个方法也是实现这个功能的。on绑定事件,传入一个回调函数,但是这里还给on加了一个功能,就是第三个参数规定回调函数执行的顺序。比如当你给同一个事件传入了多个回调函数,怎么来规定它们之间的顺序呢?通过传入第三个参数即可,数字越小的,越靠前执行。
off在解绑事件的时候,也有一个比较好的功能,可以只解绑某一个回调函数,但前提是,你在on的时候,传入的是变量名函数,解绑的时候也是这个指向函数的变量。
setter中的事件
当你使用set方法设置一个新值的时候,这个类会自动调用trigger方法去触发一个change事件,例如set('name', 'new name')的时候trigger('change:name', 'new name')会被自动触发。这和backbone的规则非常像。
同时,像getter,setter里面的层级关系也被支持了,比如set('book.name', 'Book')
,这个时候其实触发了两个事件,一个是change:book
,一个是change:book.name
,它们都会被触发,而且是两个独立的事件,绑在change:book上的回调函数和绑在change:book.name上的回调函数是完全分开的,没有任何关系,change:book事件的回调函数会被先执行。如果你不想使用这个功能,可以把set方法的第三个参数设置为false,这样就不会触发trigger。
call私有方法
ES还不支持private关键字,所以不能直接定义私有方法。这个类拥有一个.call方法,可以用来调用私有方法。私有方法写在class外面,跟attributions、events这两个变量差不多。但是在私有方法里面可以使用this,以及this携带的任何东西,在class里面call它的时候,this都是有效的。
mix一次性继承多个类
在backbone或其他一些框架中,每个类有一个extends方法来创建一个子类,在extends参数中写方法来覆盖父类的方法。但是这种操作只能继承于一个类,而如果想一次性继承几个类的某些方法,还需要自己写个扩展方法来实现。再说了,ES6本身就提供了extends关键字来继承类,所以单纯的extends方法不应该再继续使用了,只需要写一个mix方法来混入这些想要继承的类就可以了。
我写的这个类提供了两个方法,mixin和mixto,混入的方式不同。mixin是把参数里面的类的方法或属性一个一个往自己里面塞,而mixto是把自己的方法或属性往参数里面的类塞,方向上正好相反。由于塞的方向不同,最终如果方法有重名的话,被塞的一方的方法就会被保留下来,作为最终产生的混类的主体。写一个继承就非常简单:
class SubClass extends ClassBase.mixin(A, B, C) {}
mixin和mixto都是静态属性,所以可以直接用类名来调用。
toString或取类名
有的时候你想看下当前的实例化对象到底是从哪个类实例化出来的,那么直接用toString方法来获取类的名称。原本我想返回'[class BaseClass]'这种类型的字符串,但是绝对没什么意义,还不如直接返回类名,还可以用来做比较。
[…] 本文发布在我的博客求个兼职,如果您有web开发方面的需要,可以联系我,生活不容易,且行且珍惜。请在我的个人博客 http://www.tangshuang.net 留言,我会联系你。 […]