util/clone.js

'use strict';

/**
 * @author schukai GmbH
 */

import {Monster} from '../namespace.js';
import {isObject, isFunction, isPrimitive, isArray} from '../types/is.js';


/**
 * with this function, objects can be cloned.
 * the entire object tree is run through.
 *
 * Proxy, Element, HTMLDocument and DocumentFragment instances are not cloned.
 * Global objects such as windows are also not cloned,
 *
 * If an object has a method `getClone()`, this method is used to create the clone.
 *
 * you can call the method via the monster namespace `Monster.Util.clone()`.
 *
 * ```
 * <script type="module">
 * import {Monster} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.5.0/dist/modules/util/clone.js';
 * console.log(Monster.Util.clone({}))
 * </script>
 * ```
 *
 * Alternatively, you can also integrate this function individually.
 *
 * ```
 * <script type="module">
 * import {clone} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.5.0/dist/modules/util/clone.js';
 * console.log(clone({}))
 * </script>
 * ```
 *
 *
 * @param {*} obj object to be cloned
 * @returns {*}
 *
 * @since 1.0.0
 * @memberOf Monster/Util
 * @copyright schukai GmbH
 * @throws {Error} unable to clone obj! its type isn't supported.
 */
function clone(obj) {

    // typeof null results in 'object'.  https://2ality.com/2013/10/typeof-null.html
    if (null === obj) {
        return obj;
    }

    // Handle the two simple types, null and undefined
    if (isPrimitive(obj)) {
        return obj;
    }
    
    // Handle the two simple types, null and undefined
    if (isFunction(obj)) {
        return obj;
    }

    // Handle Array
    if (isArray(obj)) {
        let copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = clone(obj[i]);
        }

        return copy;
    }

    if (isObject(obj)) {


        // Handle Date
        if (obj instanceof Date) {
            let copy = new Date();
            copy.setTime(obj.getTime());
            return copy;
        }

        /** Do not clone DOM nodes */
        if (typeof Element !== 'undefined' && obj instanceof Element) return obj;
        if (typeof HTMLDocument !== 'undefined' && obj instanceof HTMLDocument) return obj;
        if (typeof DocumentFragment !== 'undefined' && obj instanceof DocumentFragment) return obj;

        /** Do not clone global objects */
        if (typeof globalContext !== 'undefined' && obj === globalContext) return obj;
        if (typeof window !== 'undefined' && obj === window) return obj;
        if (typeof document !== 'undefined' && obj === document) return obj;
        if (typeof navigator !== 'undefined' && obj === navigator) return obj;
        if (typeof JSON !== 'undefined' && obj === JSON) return obj;

        // Handle Proxy-Object
        try {
            // try/catch because possible: TypeError: Function has non-object prototype 'undefined' in instanceof check
            if (obj instanceof Proxy) {
                return obj;
            }
        } catch (e) {
        }

        return cloneObject(obj)

    }

    throw new Error("unable to clone obj! its type isn't supported.");
}

/**
 *
 * @param {object} obj
 * @returns {object}
 * @private
 */
function cloneObject(obj) {
    var copy;

    /** Object has clone method */
    if (typeof obj.hasOwnProperty('getClone') && obj.getClone === 'function') {
        return obj.getClone();
    }

    copy = {};
    if (typeof obj.constructor === 'function' &&
        typeof obj.constructor.call === 'function') {
        copy = new obj.constructor();
    }

    for (let key in obj) {

        if (!obj.hasOwnProperty(key)) {
            continue;
        }

        if (Monster.Types.isPrimitive(obj[key])) {
            copy[key] = obj[key];
            continue;
        }

        copy[key] = clone(obj[key]);
    }

    return copy;
}

Monster.assignToNamespace('Monster.Util', clone);
export {Monster, clone}