A basic class of ES6 that supports private attributes and methods, event and mix

Keywords: Javascript Attribute Web Development

ES6 provides a complete class grammar, so it is very convenient to use the extends keyword to extend (inherit) classes. In order to implement some basic functions of the class, I wrote the following class to be inherited by other classes and have the basic functions of the basic class.

Source code

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) // / binds this
                handler(...args) // It's not bound, but it can be bound with bind when on.
            }
        })

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

You can be there. Here Read the instructions for each method, and here's a brief description of their respective uses.

initialize method

To replace constructor as an instantiation method, although it is not a problem to use constructor in class, it seems that we have agreed to use initialize method to replace it. So the constructor method, as a starting method, should not be overridden in subclasses because it is used here to call initialize methods. Once overridden, initialize methods cannot be automatically invoked in subclasses.

setter and getter

It is used to acquire and set private attributes, and of course any other data type can be saved. These data will be stored in the attributions variable, but it is only visible in this document, so it will not be accessed externally, only through get and set methods.

Moreover, the function has been improved, and the incoming variables support the use of point separation to express father-son relationship. For example, set ('book. name','News') can directly set the name attribute of the book object, and get ('book'). name ='News'can also achieve this effect (higher performance), but it is not as good in form as the former, and it is more elegant to use get('book.name').

on, off and trigger

Like most event bindings and triggers, these three approaches implement this function. On binds events and passes in a callback function, but there is also a function added to on, which is that the third parameter specifies the order in which the callback function is executed. For example, when you pass multiple callback functions to the same event, how do you specify the order between them? By passing in the third parameter, the smaller the number, the more forward the execution.

off also has a better function when unbinding events, it can only unbind a callback function, but the premise is that when you are on, you pass in a variable name function, and when unbinding, it is also a variable pointing to the function.

Events in setter

When you use the set method to set a new value, the class automatically calls the trigger method to trigger a change event, such as set ('name','New name') when trigger ('change: name','New name') is triggered automatically. This is very similar to the backbone rule.

At the same time, like getter, the hierarchical relationship in setter is also supported, such as set ('book. name','book'), which actually triggers two events, one is change:book.name. They are triggered, and they are two separate events. The callback function bound to change:book and the callback function bound to change:book.name are completely separate. In any case, the callback function for the change:book event will be executed first. If you don't want to use this feature, you can set the third parameter of the set method to false, so that triggers are not triggered.

call private method

ES does not support private keywords, so private methods cannot be defined directly. This class has a. call method that can be used to call private methods. Private methods are written outside the class, similar to attributions and events. But in private methods, you can use this, and anything that this carries, and this is valid when you call it in the class.

mix inherits multiple classes at once

In backbone or other frameworks, each class has an extends method to create a subclass and write methods in the extends parameter to override the methods of the parent class. However, this operation can only be inherited from one class, and if you want to inherit some methods of several classes at once, you need to write an extension method to implement it yourself. Besides, ES6 itself provides extends keywords to inherit classes, so simple extends methods should not continue to be used, just write a mix method to mix in the classes that you want to inherit.

The class I wrote provides two methods, mixin and mixto, in different ways. Mixing is to plug the methods or attributes of the classes in the parameters into themselves one by one, while mixto is to plug its own methods or attributes into the classes in the parameters, in the opposite direction. Because the direction of plug is different, if the method has a name, the method of the plugged party will be retained as the main body of the final mixed class. Writing an inheritance is very simple:

class SubClass extends ClassBase.mixin(A, B, C) {}

Mixing in and mixto are static attributes, so they can be invoked directly with class names.

To String Gets the Class Name

Sometimes you want to see which class the current instantiated object is instantiated from, so use the toString method to get the name of the class directly. Originally, I wanted to return a string of the type'[class BaseClass]', but it was absolutely meaningless. It would be better to return the class name directly and use it for comparison.

This article is published in My blog
Seek a part-time job, if you have the needs of web development, you can contact me, life is not easy, and line and cherish.
Please visit my personal blog www.tangshuang.net Leave a message. I'll contact you.

Posted by usacascl on Wed, 17 Apr 2019 18:21:34 -0700