Home Reference Source

src/utils/ObjectHelper.js

/**


 @author: Tom Clement (tjclement)
 @license NPOSL-3.0
 @copyright Bizboard, 2015

 */

import merge                from 'lodash/merge.js';
import extend               from 'lodash/extend.js';

export class ObjectHelper {

    /* Sets enumerability of methods and all properties starting with '_' on an object to false,
     * effectively hiding them from for(x in object) loops.   */
    static hideMethodsAndPrivatePropertiesFromObject(object) {
        for (let propName in object) {

            let prototype = Object.getPrototypeOf(object);
            let descriptor = prototype ? Object.getOwnPropertyDescriptor(prototype, propName) : undefined;
            if (descriptor && (descriptor.get || descriptor.set) && !propName.startsWith('_')) {
                /* This is a public getter/setter, so we can skip it */
                continue;
            }

            let property = object[propName];
            if (typeof property === 'function' || propName.startsWith('_')) {
                ObjectHelper.hidePropertyFromObject(object, propName);
            }
        }
    }

    /* Sets enumerability of methods on an object to false,
     * effectively hiding them from for(x in object) loops.   */
    static hideMethodsFromObject(object) {
        for (let propName in object) {
            let property = object[propName];
            if (typeof property === 'function') {
                ObjectHelper.hidePropertyFromObject(object, propName);
            }
        }
    }

    /* Sets enumerability of an object's property to false,
     * effectively hiding it from for(x in object) loops.   */
    static hidePropertyFromObject(object, propName) {
        let prototype = object;
        let descriptor = Object.getOwnPropertyDescriptor(object, propName);
        while (!descriptor) {
            prototype = Object.getPrototypeOf(prototype);

            if (prototype.constructor.name === 'Object' || prototype.constructor.name === 'Array') {
                return;
            }

            descriptor = Object.getOwnPropertyDescriptor(prototype, propName);
        }
        descriptor.enumerable = false;
        Object.defineProperty(prototype, propName, descriptor);
        Object.defineProperty(object, propName, descriptor);
    }

    /* Sets enumerability of all of an object's properties (including methods) to false,
     * effectively hiding them from for(x in object) loops.   */
    static hideAllPropertiesFromObject(object) {
        for (let propName in object) {
            ObjectHelper.hidePropertyFromObject(object, propName);
        }
    }

    /* Adds a property with enumerable: false to object */
    static addHiddenPropertyToObject(object, propName, prop, writable = true, useAccessors = true) {
        return ObjectHelper.addPropertyToObject(object, propName, prop, false, writable, undefined, useAccessors);
    }

    /* Adds a property with given enumerability and writability to object. If writable, uses a hidden object.shadow
     * property to save the actual data state, and object[propName] with gettter/setter to the shadow. Allows for a
     * callback to be triggered upon every set.   */
    static addPropertyToObject(object, propName, prop, enumerable = true, writable = true, setCallback = null, useAccessors = true) {
        /* If property is non-writable, we won't need a shadowed prop for the getters/setters */
        if (!writable || !useAccessors) {
            let descriptor = {
                enumerable: enumerable,
                writable: writable,
                value: prop
            };
            Object.defineProperty(object, propName, descriptor);
        } else {
            ObjectHelper.addGetSetPropertyWithShadow(object, propName, prop, enumerable, writable, setCallback);
        }
    }

    /* Adds given property to the object with get() and set() accessors, and saves actual data in object.shadow */
    static addGetSetPropertyWithShadow(object, propName, prop, enumerable = true, writable = true, setCallback = null) {
        ObjectHelper.buildPropertyShadow(object, propName, prop);
        ObjectHelper.buildGetSetProperty(object, propName, enumerable, writable, setCallback);
    }

    /* Creates or extends object.shadow to contain a property with name propName */
    static buildPropertyShadow(object, propName, prop) {
        let shadow = {};

        try {
            /* If a shadow property already exists, we should extend instead of overwriting it. */
            if ('shadow' in object) {
                shadow = object.shadow;
            }
        } catch (error) {
            return;
        }

        shadow[propName] = prop;
        Object.defineProperty(object, 'shadow', {
            writable: true,
            configurable: true,
            enumerable: false,
            value: shadow
        });
    }

    /* Creates a property on object that has a getter that fetches from object.shadow,
     * and a setter that sets object.shadow as well as triggers setCallback() if set.   */
    static buildGetSetProperty(object, propName, enumerable = true, writable = true, setCallback = null) {
        let descriptor = {
            enumerable: enumerable,
            configurable: true,
            get: function () {
                return object.shadow[propName];
            },
            set: function (value) {
                if (writable) {
                    object.shadow[propName] = value;
                    if (setCallback && typeof setCallback === 'function') {
                        setCallback({
                            propertyName: propName,
                            newValue: value
                        });
                    }
                } else {
                    throw new ReferenceError('Attempted to write to non-writable property ' + propName + '.');
                }
            }
        };

        Object.defineProperty(object, propName, descriptor);
    }

    /* Calls object['functionName'].bind(bindTarget) on all of object's functions. */
    static bindAllMethods(object, bindTarget) {

        /* Bind all current object's methods to bindTarget. */
        let methodDescriptors = ObjectHelper.getMethodDescriptors(object);
        for (let methodName in methodDescriptors) {
            /* Skip the constructor as it serves as no purpose and it breaks the minification */
            if(methodName === 'constructor'){
                continue;
            }
            let propertyDescriptor = methodDescriptors[methodName];
            if (propertyDescriptor && propertyDescriptor.get) {
                propertyDescriptor.get = propertyDescriptor.get.bind(bindTarget);
            } else if (propertyDescriptor.set) {
                propertyDescriptor.set = propertyDescriptor.set.bind(bindTarget);
            } else if (propertyDescriptor.writable) {
                propertyDescriptor.value = propertyDescriptor.value.bind(bindTarget);
            }
            Object.defineProperty(object, methodName, propertyDescriptor);
        }
    }


    static getMethodDescriptors(object) {

        let methodDescriptors = {};

        for (let propertyName of Object.getOwnPropertyNames(object)) {
            let propertyDescriptor = Object.getOwnPropertyDescriptor(object, propertyName) || {};
            /* Initializers can be ignored since they are bound anyways */
            if (!propertyDescriptor.initializer && (propertyDescriptor.get || typeof object[propertyName] === 'function')) {
                methodDescriptors[propertyName] = propertyDescriptor;
            }
        }

        /* Recursively find prototype's methods until we hit the Object prototype. */
        let prototype = Object.getPrototypeOf(object);
        if (prototype.constructor.name !== 'Object' && prototype.constructor.name !== 'Array') {
            methodDescriptors = extend(ObjectHelper.getMethodDescriptors(prototype), methodDescriptors);
        }

        return methodDescriptors;

    }

    /* Returns a new object with all enumerable properties of the given object */
    static getEnumerableProperties(object) {

        return ObjectHelper.getPrototypeEnumerableProperties(object, object);

    }

    static getPrototypeEnumerableProperties(rootObject, prototype) {
        let result = {};

        /* Collect all propertise in the prototype's keys() enumerable */
        let propNames = Object.keys(prototype);
        for (let name of propNames) {
            let value = rootObject[name];

            /* Value must be a non-null primitive or object to be pushable to a dataSource */
            if (value !== null && value !== undefined && typeof value !==
                'function') {
                if (typeof value === 'object' && !(value instanceof Array)) {
                    result[name] = ObjectHelper.getEnumerableProperties(value);
                } else {
                    result[name] = value;
                }
            }
        }

        /* Collect all properties with accessors (getters/setters) that are enumerable, too */
        let descriptorNames = Object.getOwnPropertyNames(prototype);
        descriptorNames = descriptorNames.filter(function (name) {
            return propNames.indexOf(name) < 0;
        });
        for (let name of descriptorNames) {
            let descriptor = Object.getOwnPropertyDescriptor(prototype, name);
            if (descriptor && descriptor.enumerable) {
                let value = rootObject[name];

                /* Value must be a non-null primitive or object to be pushable to a dataSource */
                if (value !== null && value !== undefined && typeof value !== 'function') {
                    if (typeof value === 'object' && !(value instanceof Array)) {
                        result[name] = ObjectHelper.getEnumerableProperties(value);
                    } else {
                        result[name] = value;
                    }
                }
            }
        }

        /* Collect all enumerable properties in the prototype's prototype as well */
        let superPrototype = Object.getPrototypeOf(prototype);
        let ignorableTypes = ['Object', 'Array', 'EventEmitter'];
        if (ignorableTypes.indexOf(superPrototype.constructor.name) === -1) {
            let prototypeEnumerables = ObjectHelper.getPrototypeEnumerableProperties(rootObject, superPrototype);
            merge(result, prototypeEnumerables);
        }

        return result;
    }
}