ES6的一個基礎類,支持私有屬性和方法,支持event和mix

ES6提供了完整的class語法,所以,能夠很是方便的使用extends關鍵字對類進行擴展(繼承)。爲了實現類的一些基礎功能,我撰寫了下面這個類,用以被其餘類繼承,擁有這個基礎類的基礎功能。html

源碼

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
    }
}

你能夠在這裏閱讀每個方法的說明,這裏簡單的說明一下它們的各自用途。node

initialize方法

用來替代constructor做爲實例化方法,雖然在class中使用constructor並無什麼問題,可是你們彷佛約定熟成的使用initialize方法替換它。因此constructor方法做爲一個最起始的方法,不該該在子類中出現被覆蓋,由於這裏會用它來調用initialize方法,一旦被覆蓋,子類中就不能自動調用initialize方法了。git

setter和getter

用以獲取和設置私有屬性,固然也能夠保存其餘任何數據類型。這些數據都會被保存在attributions這個變量裏面,可是它僅在這個文檔中可見,因此不會被外部訪問,只有經過get和set方法才能訪問。github

並且功能有所提高,傳入的變量支持用點隔開來表示父子關係。好比set('book.name', 'News'),這樣能夠直接設置book對象的name屬性,用get('book').name = 'News'也能夠達到這個效果(性能更高),但形式上沒有前者好看,使用get('book.name')這種寫法也更優雅。web

on、off和trigger

和大多數事件綁定和觸發同樣,這三個方法也是實現這個功能的。on綁定事件,傳入一個回調函數,可是這裏還給on加了一個功能,就是第三個參數規定回調函數執行的順序。好比當你給同一個事件傳入了多個回調函數,怎麼來規定它們之間的順序呢?經過傳入第三個參數便可,數字越小的,越靠前執行。app

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都是有效的。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開發方面的須要,能夠聯繫我,生活不容易,且行且珍惜。
請在個人我的博客 www.tangshuang.net 留言,我會聯繫你。

相關文章
相關標籤/搜索