BELT vs Underscore isequal (v12)

Revision 12 of this benchmark created by John Cockrell on


Preparation HTML

<script src="http://documentcloud.github.com/underscore/underscore-min.js"></script>
<script>
/*
*   belt.js
*
*   A general-purpose utility function. Used to extend
*     and house my general-purpose js-utils.
*
*/

//  Testing shim.
//  Node or DOM.
var global = global || window;


(function(global) {

/*
*   SETUP
*
*   Initialize public and private variables, and overwrite
*     the `global.BELT` namespace, caching anything that might be there.
*   Orient document, navigator, location to the global object, since we're
*     operating inside a self-invoking function closure.
*
*   BELT     : Global namespace for utility functions.
*
*   _cache   : A private cache object accessed by BELT fns.
*   _errors  : A private error queue; populated by BELT error logging.
*   _belt    : A copy of anything in the `global.BELT` namespace, in case
*              we end up overwriting something.
*   _console : A copy of the `global.console` if it exists. BELT defaults to
*              creating a safe copy of console in the global namespace,
*              overwriting the normal `global.console`.
*
*/
    var BELT = {},
        _cache = {},
        _errors = [],

        //  Make sure document, location, navigator are properly defined
        //    within the scope of this self-invoking function.
        document = global.document,
        location = global.location,
        navigator = global.navigator;

    //  Since we'll be overwriting global.BELT and global.console
    //    make the old values accessible via the _cache.
    _cache.global_BELT = global.BELT;
    _cache.global_console = global.console;

/* ---------------- top ----------------- */

/*
*   ECMA Shims
*
*   Date.now()
*       Not available in LT ECMA1.5
*       Creates version of Date.now if not supported.
*       Perf testing: `new Date().getTime()` > `+(new Date)` by a lot.
*       http://jsperf.com/creating-timestamps
*
*   Array.indexOf()
*       This method is much more efficient at telling whether an object
*       exists in an array--especially if it's a complex object.
*       Method not supported/partially supported in >= IE9, and is worth
*       shimming.
*       Uses Mozilla ECMA-262 shim, see:
*       https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf
*
*/

    Date.now = Date.now || function now() { return new Date().getTime() };

    //  Precompressed to save space. 
    Array.prototype.indexOf = Array.prototype.indexOf || 
        function indexOf(d){
            var c,b,a;if(void 0===this||null===this)throw new TypeError;
            c=Object(this);b=c.length>>>0;if(0===b)return-1;a=0;
            0<arguments.length&&(a=Number(arguments[1]),a!==a?a=0:0!==a&&
            (a!==1/0&&a!==-(1/0))&&(a=(0<a||-1)*Math.floor(Math.abs(a))));
            if(a>=b)return-1;for(a=0<=a?a:Math.max(b-Math.abs(a),0);a<b;a++)
            {if(a in c&&c[a]===d)return a;}return -1;
        };


/* ------------- ecma shim -------------- */

/*
*   CORE JS UTILITIES
*
*   Borrow various native functions for internal use.
*   Any self-rolled object and array manipulation functions go here.
*
*   slice()
*       Array.prototype.slice
*
*   push()
*       Array.prototype.push
*
*   toString()
*       Object.prototype.toString
*
*   hasOwnProperty()
*       Object.prototype.hasOwnProperty
*
*
*   typeof x functions
*       Using type-check functions is a hell of a lot more efficient than
*       the string parsing that Underscore does (at least as of 1.4.3).
*       http://jsperf.com/is-it-a-number
*       Object, Function, String, Number
*           @argument : thing
*           @return   : [boolean]
*
*   instanceof functions
*       Checking things that can't be checked with typeof.
*       Array, Element, Date, Regex, Error
*           @argument : thing
*           @return   : [boolean]
*
*   areArraysEqual()
*       Checks if the arrays are equal. Because this is where JS sucks.
*           @arguments : Array A, Array B
*           @return    : boolean
*
*   isEmpty()
*       Checks whether a thing is "empty". 
*       Checking if Functions are empty is somewhat strange, has 3 criteria:
*       1. no name,
*       2. no arguments,
*       3. no operations in curly braces.
*       Roughly a port of Underscore's _.isEmpty() fn.
*       h/t Jeremy Ashkenas. http://underscorejs.org
*           @arguments : something
*           @return    : [boolean]
*
*   clone()
*       Create a deep copy of an object via recursion.
*       This is more effective than JSON.* means of cloning objects,
*         since cloned functions are usable.
*       This is for non-prototypal object literals and arrays, rather
*         than constructed objects... for the moment.
*       Currently outstrips jQuery.extend() [v 1.8.3] in performance
*         by a significant margin : http://jsperf.com/belt-core-deep-clone
*           @arguments : from [,useHard]
*           @return    : [clone of object]
*
*
*   extend()
*           @dep       : core.clone(), core.areArraysEqual()
*           @arguments : target, object [,useHard]
*           @return    : boolean
*
*
*   namespace()
*       Return the tip of a namespace. If it doesn't exist yet, then
*         create an object in this namespace.
*       Using `BELT.namespace()` prevents accidental rewrites when
*         working with big namespaces.
*           @arguments : [string]
*           @return    : [Object object]
*
*   inherit()
*       A temporary-constructor inheritance pattern. 
*           @arguments : parent, child.
*                        {parent:, child:}
*           @return    : ...
*
*
*   createConsoleAlias()
*       The JavaScript global console object is not consistently implemented
*       cross brwoser yet.
*       This creates a `console` object to prevent script crashes when you
*       attempt to access a method of an undefined object. Methods are aliased
*       in stages based on browser availability.
*       As far as I know, I've grouped the commands by availability in different
*       browser environments. The most generic set, `ie_opera_ff_webkit` will
*       work pretty reliably x-browser when you use it.
*           @arguments : none
*           @return    : Pseudo-console.
*
*/
BELT.core = {};
    BELT.slice = BELT.core.slice = Array.prototype.slice;
    BELT.push = BELT.core.push = Array.prototype.push;
    BELT.toString = BELT.core.toString = Object.prototype.toString;
    BELT.hasOwnProperty = BELT.core.hasOwnProperty = Object.prototype.hasOwnProperty;
    
    BELT.isObject = BELT.core.isObject = function(it) {
        return typeof it === 'object';
    };

    BELT.isFunction = BELT.core.isFunction = function(it) {
        return typeof it === 'function';
    };

    BELT.isString = BELT.core.isString = function(it) {
        return typeof it === 'string';
    };

    BELT.isNumber = BELT.core.isNumber = function(it) {
        return typeof it === 'number';
    };

    BELT.isBoolean = BELT.core.isBoolean = function(it) {
        return typeof it === 'boolean';
    };

    BELT.isUndefined = BELT.core.isUndefined = function(it) {
        return typeof it === 'undefined';  
    };

    BELT.isArray = BELT.core.isArray = function(it) {
        return it instanceof Array;
    };

    BELT.isElement = BELT.core.isElement = function(it) {
        return it instanceof Element;
    };

    BELT.isDate = BELT.core.isDate = function(it) {
        return it instanceof Date;
    };

    BELT.isRegex = BELT.core.isRegex = function(it) {
        return it instanceof Regex;
    };

    BELT.isError = BELT.core.isError = function(it) {
        return it instanceof Error;
    };

    BELT.areArraysEqual = BELT.core.areArraysEqual = function(a, b) {
        return !(a<b || b>a);
    };


    BELT.isEmpty = BELT.core.isEmpty = function(it) {
        var prop;

        if (it === undefined) { 
            return true; 

        } else if (BELT.core.isNumber(it)) { 
            return false; 

        } else if (BELT.core.isArray(it) || BELT.core.isString(it)) { 
            return it.length === 0;

        } else if (BELT.core.isFunction(it)) {
            return it.name === '' && (/\(\s*\)\s*{\s*}/).test(a);

        } else if (BELT.core.isObject(it)) {
            for (prop in it) { 
                if (it.hasOwnProperty(prop)) { return false; }
            }
        }

        return true;
    };



        BELT.isEqual_d1 = BELT.core.isEqual_d1 = (function(){ 
            function recurse(a_i, b_i) {
                //console.log('recursing...');
                //console.log('a_i : ');
                //console.log(a_i);
                //console.log('b_i : ');
                //console.log(b_i);
                if (BELT.core.isObject(a_i)) {
                    //console.log('array or object at this unit. go deep.');
                    return BELT.core.isEqual_d1(a_i, b_i);
                } else {
                    //console.log('primitives. equate.');
                    return a_i === b_i;
                }
            };
    
            return function isEqual_d1(a, b) {
    
                var type_a,
                    type_b,
                    constructor_a, 
                    constructor_b,
                    prop,
                    i,
                    k;
    
                type_a = typeof a;
                type_b = typeof b;
                constructor_a = a.constructor;
                constructor_b = b.constructor;
    
                //  TYPECHECK
                //  Do a/b exist?
                //  This we can't check for, since we might be testing null vs
                //  undefined.
                //  BUT, we can check types to make sure this is worth processing
                //  at all. If they're the wrong types altogether, who cares?
                //  This will encapsulate the null/undefined test case.
                //  
                //  Check primitives (typeof)
                if (type_a !== type_b) { return false; }
                //console.log('Pass: primitives.');
    
                //  Check advanced types, using constructors.
                if (constructor_a !== constructor_b) { return false; }
                //console.log('Pass: constructors.');
    
    
                //  !!! WARNING - DO NOT UNDERSTAND DO NOT WANT
                //  
                //  0 , -0 IDENTITY CASE
                //  "Equal" in JavaScript, but not the same.
                if (a === b) { return a !== 0 || 1/a == 1/b; }
                //console.log('Pass: identity case.');
    
    
                //  EMPTY
                //  If they're empty, do all that before we start manipulating
                //  strings and other pricey procedures.
                if (BELT.isEmpty(a) && BELT.isEmpty(b)) { return true; }
                //console.log('Pass: empty array/object.');
    
    
                //  PRIMITIVE EQUIVALENCES
                //  Comparison switch for comparable primitive types.
                if (type_a === 'string' || type_a === 'boolean' || type_a === 'number') {
                    return a === b;
                }
                //console.log('Pass: primitive eq.');
    
    
                //  COMPLEX TYPE EQUIVALENCE
                //  Comparison for comparable complex types for things that 
                //  aren't objects.
                //  Dates
                //      Convert into comparable units, AKA milliseconds, to judge.
                //  RegExp
                //      Deconstruct component parts to see if they are eq all through.
                //      h/t Jeremy Ashkenas, Underscore.
                //  Function
                //      Similar to isEmpty(), we compare two things:
                //      - name
                //      - stringified function contents
                //      a = function Panda(){}
                //      b = function Fox(){}
                //      a eq b 
                //          => false
                //
                //      a = function(   ){ }
                //      b = function (){     }
                //      a eq b 
                //          => true
                //
                switch (constructor_a) {
                    case Date:
                        return a.getTime() == b.getTime();
    
                    case RegExp:
                        return a.source == b.source && 
                               a.global == b.global && 
                               a.multiline == b.multiline &&
                               a.ignoreCase == b.ignoreCase;
                    
                    case Function:
                        return a.name === b.name &&
                                (a.toString() === b.toString() || 
                                 BELT.isEmpty(a) && BELT.isEmpty(b));
                   }
                //console.log('Pass: simple (comparative) complex-type equivalence.');
    
    
                //  LENGTH EVALUATION
                //  Does this array have the same number of elements? or, does
                //  this object have the same number of properties?
                if (constructor_a === Array && a.length !== b.length) {
                    return false;
                }
                //console.log('Pass: simple length complex-type equivalence.');
    
    
                //  RECURSAL EVALUATION
                //  If we end up with two things that aren't equal,
                //  break out as false as soon as you do.
                if (constructor_a === Array) {
                    //console.log('Recursal eval: Array');
                    for (i = 0; i < a.length; i += 1) {
                        if (recurse(a[i], b[i]) === false) { return false; }
                    }
    
                } else if (constructor_a === Object) {
                    //console.log('Recursal eval: object');
                    for (i in a) {
                        if (recurse(a[i], b[i]) === false) { return false; }
                    }
                }
    
                //  OK.
                //  We threw our best at it, and it didn't budge. It's probably equal.
                return true;
            };
    
        }());




    //  Adapted from Underscore's isEqual
    //  h/t Jeremy Askenas
    //
    //  What does it mean to be equal?
    //  Strictly equivalent.
    //  
    //  1 eq "1" => false
    //  null eq undefined => false

    //  NOTE: Requires strict type checking, not just 'truthiness'
    //  1 eq true => false

    BELT.isEqual_d3 = BELT.core.isEqual_d3 = (function(){ 
        function recurse(a_i, b_i) {
            if (BELT.core.isObject(a_i)) {
                return BELT.core.isEqual_d3(a_i, b_i);
            } else {
                return a_i === b_i;
            }
        };

        return function isEqual_d3(a, b) {
            var type_a,
                type_b,
                constructor_a, 
                constructor_b,
                prop,
                i;

            type_a = typeof a;
            type_b = typeof b;
            constructor_a = a.constructor;
            constructor_b = b.constructor;

            //  TYPECHECK
            //  Do a/b exist?
            //  This we can't check for, since we might be testing null vs
            //  undefined.
            //  BUT, we can check types to make sure this is worth processing
            //  at all. If they're the wrong types altogether, who cares?
            //  This will encapsulate the null/undefined test case.
            //  
            //  Check primitives (typeof)
            if (type_a !== type_b) { return false; }

            //  Check advanced types, using constructors.
            if (constructor_a !== constructor_b) { return false; }


            //  0 , -0 IDENTITY CASE
            //  "Equal" in JavaScript, but not the same.
            //  h/t Jeremy Askenas, Underscore.
            if (a === b) { return a !== 0 || 1/a == 1/b; }


            

            //  PRIMITIVE EQUIVALENCES
            //  Comparison switch for comparable primitive types.
            if (type_a === 'string') {
                return a === b;

            } else if (type_a === 'boolean'){ 
                return a === b;

            } else if (type_a === 'number') {
                return a === b;

            }
            
            //  EMPTY
            //  If they're empty, do all that before we start manipulating
            //  strings and other pricey procedures.
            if (BELT.core.isEmpty(a) && BELT.core.isEmpty(b)) { return true; }


            //  COMPLEX TYPE EQUIVALENCE
            //  Comparison for comparable complex types for things that 
            //  aren't objects.
            //  Dates
            //      Convert into comparable units, AKA milliseconds, to judge.
            //  RegExp
            //      Deconstruct component parts to see if they are eq all through.
            //      h/t Jeremy Ashkenas, Underscore.
            //  Function
            //      Similar to isEmpty(), we compare two things:
            //      - name
            //      - stringified function contents
            //      a = function Panda(){}
            //      b = function Fox(){}
            //      a eq b 
            //          => false
            //
            //      a = function(   ){ }
            //      b = function (){     }
            //      a eq b 
            //          => true
            //

            if (constructor_a === Date) {
                return a.getTime() == b.getTime();

            } else if (constructor_a === RegExp) {
                return a.source == b.source && 
                       a.global == b.global && 
                       a.multiline == b.multiline &&
                       a.ignoreCase == b.ignoreCase;

            } else if (constructor_a === Function) {
                 return a.name === b.name &&
                        (a.toString() === b.toString() || 
                         BELT.isEmpty(a) && BELT.isEmpty(b));
            }

            //  RECURSAL AND LENGTH EVALUATION
            //  If we end up with two things that aren't equal,
            //  break out as false as soon as you do.
            //
            //  Does this array have the same number of elements? or, does
            //  this object have the same number of properties?
            if (constructor_a === Array) {
                len = a.length;
                if (len !== b.length) { return false; }
                for (i = 0; i < len; i += 1) {
                    if (recurse(a[i], b[i]) === false) { return false; }
                }

            } else if (constructor_a === Object) {
                for (i in a) {
                    if (recurse(a[i], b[i]) === false) { return false; }
                }
            }

            //  OK.
            //  We threw our best at it, and it didn't budge. It's probably equal.
            return true;
        };

    }());




//  Adapted from Underscore's isEqual
    //  h/t Jeremy Askenas
    //
    //  What does it mean to be equal?
    //  Strictly equivalent.
    //  
    //  1 eq "1" => false
    //  null eq undefined => false

    //  NOTE: Requires strict type checking, not just 'truthiness'
    //  1 eq true => false

    BELT.isEqual_d4 = BELT.core.isEqual_d4 = (function(){ 
        function recurse(a_i, b_i) {
            if (BELT.core.isObject(a_i)) {
                return BELT.core.isEqual_d4(a_i, b_i);
            } else {
                return a_i === b_i;
            }
        };

        return function isEqual_d4(a, b) {
            var type_a,
                type_b,
                constructor_a, 
                constructor_b,
                prop,
                i;

            type_a = typeof a;
            type_b = typeof b;
            constructor_a = a.constructor;
            constructor_b = b.constructor;

            //  TYPECHECK
            //  Do a/b exist?
            //  This we can't check for, since we might be testing null vs
            //  undefined.
            //  BUT, we can check types to make sure this is worth processing
            //  at all. If they're the wrong types altogether, who cares?
            //  This will encapsulate the null/undefined test case.
            //  
            //  Check primitives (typeof)
            if (type_a !== type_b || constructor_a !== constructor_b) { 
                return false; 
            }

            //  0 , -0 IDENTITY CASE
            //  "Equal" in JavaScript, but not the same.
            //  h/t Jeremy Askenas, Underscore.
            if (a === b) { return a !== 0 || 1/a == 1/b; }

            //  PRIMITIVE EQUIVALENCES
            //  Comparison switch for comparable primitive types.
            //  We've already seen that a === b .. > false, otherwise, 
            //  we would have gone into the if statement up there.
            if (type_a === 'string' || type_a === 'boolean' || type_a === 'number') {
                return false;
            }
            
            //  COMPLEX TYPE EQUIVALENCE
            //  Comparison for comparable complex types for things that 
            //  aren't objects.
            //  Dates
            //      Convert into comparable units, AKA milliseconds, to judge.
            //  RegExp
            //      Deconstruct component parts to see if they are eq all through.
            //      h/t Jeremy Ashkenas, Underscore.
            //  Function
            //      Similar to isEmpty(), we compare two things:
            //      - name
            //      - stringified function contents
            //      a = function Panda(){}
            //      b = function Fox(){}
            //      a eq b 
            //          => false
            //
            //      a = function(   ){ }
            //      b = function (){     }
            //      a eq b 
            //          => true
            //

            if (constructor_a === Date) {
                return a.getTime() == b.getTime();

            } else if (constructor_a === RegExp) {
                return a.source == b.source && 
                       a.global == b.global && 
                       a.ignoreCase == b.ignoreCase &&
                       a.multiline == b.multiline;

            } else if (constructor_a === Function) {
                 return a.name === b.name &&
                        (a.toString() === b.toString() || 
                         BELT.isEmpty(a) && BELT.isEmpty(b));
            
            //  continues below...

            //  RECURSAL AND LENGTH EVALUATION
            //  If we end up with two things that aren't equal,
            //  break out as false as soon as you do.
            //
            //  Does this array have the same number of elements? or, does
            //  this object have the same number of properties?
            } else if (constructor_a === Array) {
                if (a.length !== b.length) { return false; }
                len = a.length;
                for (i = 0; i < len; i += 1) {
                    if (recurse(a[i], b[i]) === false) { return false; }
                }

            } else if (constructor_a === Object) {
                for (i in a) {
                    if (!b[i]) { return false; }    //Test keys first.
                    if (recurse(a[i], b[i]) === false) { return false; }
                }
            }

            //  OK.
            //  We threw our best at it, and it didn't budge. It's probably equal.
            return true;
        };

    }());


    BELT.isEqual_d6 = BELT.core.isEqual_d6 = (function(){ 
        function recurse(a_i, b_i) {
            if (BELT.core.isObject(a_i)) {
                return BELT.core.isEqual_d6(a_i, b_i);
            } else {
                return a_i === b_i;
            }
        };

        return function isEqual_d6(a, b) {
            var type_a,
                type_b,
                constructor_a, 
                constructor_b,
                len_a,
                prop,
                i;

            //  0 , -0 IDENTITY CASE
            //  "Equal" in JavaScript, but not the same.
            //  h/t Jeremy Askenas, Underscore.
            if (a === b) { return a !== 0 || 1/a == 1/b; }


            //  TYPECHECK
            //  Do a/b exist?
            //  This we can't check for, since we might be testing null vs
            //  undefined.
            //  BUT, we can check types to make sure this is worth processing
            //  at all. If they're the wrong types altogether, who cares?
            //  This will encapsulate the null/undefined test case.
            //  
            //  Check primitives (typeof)

            type_a = typeof a;
            type_b = typeof b;
            constructor_a = a.constructor;
            constructor_b = b.constructor;


            if (type_a !== type_b) { return false; }
            if (constructor_a !== constructor_b) { return false; }


            
            
            //  PRIMITIVE EQUIVALENCES
            //  Comparison switch for comparable primitive types.
            if (type_a === 'string') {
                return a === b;

            } else if (type_a === 'boolean'){ 
                return a === b;

            } else if (type_a === 'number') {
                return a === b;

            }
            
            //  EMPTY
            //  If they're empty, do all that before we start manipulating
            //  strings and other pricey procedures.
            // if (BELT.core.isEmpty(a) && BELT.core.isEmpty(b)) { return true; }

            //  COMPLEX TYPE EQUIVALENCE
            //  Comparison for comparable complex types for things that 
            //  aren't objects.
            //  Dates
            //      Convert into comparable units, AKA milliseconds, to judge.
            //  RegExp
            //      Deconstruct component parts to see if they are eq all through.
            //      h/t Jeremy Ashkenas, Underscore.
            //  Function
            //      Similar to isEmpty(), we compare two things:
            //      - name
            //      - stringified function contents
            //      a = function Panda(){}
            //      b = function Fox(){}
            //      a eq b 
            //          => false
            //
            //      a = function(   ){ }
            //      b = function (){     }
            //      a eq b 
            //          => true
            //

            if (constructor_a === Date) {
                return a.getTime() == b.getTime();

            } else if (constructor_a === RegExp) {
                return a.source == b.source && 
                       a.global == b.global && 
                       a.ignoreCase == b.ignoreCase &&
                       a.multiline == b.multiline;

            } else if (constructor_a === Function) {
                 return a.name === b.name &&
                        (a.toString() === b.toString() || 
                         BELT.isEmpty(a) && BELT.isEmpty(b));
            
            //  continues below...

            //  RECURSAL AND LENGTH EVALUATION
            //  If we end up with two things that aren't equal,
            //  break out as false as soon as you do.
            //
            //  Does this array have the same number of elements? or, does
            //  this object have the same number of properties?
            } else if (constructor_a === Array) {
                len_a = a.length;
                if (len_a !== b.length) { return false; }
                for (i = 0; i < len_a; i += 1) {
                    if (recurse(a[i], b[i]) === false) { return false; }
                }

            } else if (constructor_a === Object) {
                for (i in a) {
                    if (!b[i]) { return false; }
                    if (recurse(a[i], b[i]) === false) { return false; }
                }
            }

            //  OK.
            //  We threw our best at it, and it didn't budge. It's probably equal.
            return true;
        };

    }());




    //  D7 Draft
    //  
    //  Observations:
    //  - Constructor access is really slow.
    //  - Use primitive/complex type filtering early on
    //    (sort boolean, number, undefined, etc. and process
    //    differently from objects)
    //  - Don't run constructor tests; run instanceof on any complex obj.

    BELT.isEqual_d7 = BELT.core.isEqual_d7 = (function(){ 
        function recurse(a_i, b_i) {
            if (BELT.core.isObject(a_i)) {
                return BELT.core.isEqual_d7(a_i, b_i);
            } else {
                return a_i === b_i;
            }
        };

        return function isEqual_d7(a, b) {
            var type_a,
                type_b,
                constructor_a, 
                constructor_b,
                len_a,
                prop,
                i;




            //  TYPECHECK
            //  Do a/b exist?
            //  This we can't check for, since we might be testing null vs
            //  undefined.
            //  BUT, we can check types to make sure this is worth processing
            //  at all. If they're the wrong types altogether, who cares?
            //  This will encapsulate the null/undefined test case.
            //  
            //  Check primitives (typeof)
            type_a = typeof a;
            type_b = typeof b;
            if (type_a !== type_b) { return false; }


            //  PRIMITIVES AND MEMORY EQUIVALENCE
            //  "Equal" in JavaScript -- pointing to same space in memory,
            //  or two primitives.
            //  h/t Jeremy Askenas, Underscore.
            if (a === b) { return a !== 0 || 1/a == 1/b; }


            
            //  COMPLEX OBJECTS
            constructor_a = a.constructor;
            constructor_b = b.constructor;
            if (constructor_a !== constructor_b) { return false; }


            /*  -------------- */

            //  COMPLEX TYPE EQUIVALENCE
            //  Comparison for comparable complex types for things that 
            //  aren't objects.
            //  Dates
            //      Convert into comparable units, AKA milliseconds, to judge.
            //  RegExp
            //      Deconstruct component parts to see if they are eq all through.
            //      h/t Jeremy Ashkenas, Underscore.
            //  Function
            //      Similar to isEmpty(), we compare two things:
            //      - name
            //      - stringified function contents
            //      a = function Panda(){}
            //      b = function Fox(){}
            //      a eq b 
            //          => false
            //
            //      a = function(   ){ }
            //      b = function (){     }
            //      a eq b 
            //          => true
            //
            if (constructor_a === Date) {
                return a.getTime() == b.getTime();

            } else if (constructor_a === RegExp) {
                return a.source == b.source && 
                       a.global == b.global && 
                       a.ignoreCase == b.ignoreCase &&
                       a.multiline == b.multiline;

            } else if (constructor_a === Function) {
                 return a.name === b.name &&
                        (a.toString() === b.toString() || 
                         BELT.isEmpty(a) && BELT.isEmpty(b));
            
            //  continues below...

            //  RECURSAL AND LENGTH EVALUATION
            //  If we end up with two things that aren't equal,
            //  break out as false as soon as you do.
            //
            //  Does this array have the same number of elements? or, does
            //  this object have the same number of properties?
            } else if (constructor_a === Array) {
                len_a = a.length;
                if (len_a !== b.length) { return false; }
                for (i = 0; i < len_a; i += 1) {
                    if (recurse(a[i], b[i]) === false) { return false; }
                }

            } else if (constructor_a === Object) {
                for (i in a) {
                    if (!b[i]) { return false; }
                    if (recurse(a[i], b[i]) === false) { return false; }
                }
            }

            //  OK.
            //  We threw our best at it, and it didn't budge. It's probably equal.
            return true;
        };


    }());


    BELT.clone = BELT.core.clone = (function() {

        //  Private helper function for public clone() API.
        function recurse(from_i, doppel_i) {
            //  isObject will catch arrays and objects, both of which we 
            //  need to loop on.
            if (BELT.core.isObject(from_i)) {
                return BELT.core.clone(from_i, doppel_i);
            } else {
                return from_i;
            }
        }

        return function clone(from, useHard) {
            var doppel,
                len,
                i;

            if (BELT.core.isArray(from)) {
                doppel = [];
                len = from.length;
                for (i = 0; i < len; i += 1) {
                    doppel[i] = recurse(from[i], doppel[i]);
                }

            } else if (BELT.core.isObject(from)) {
                doppel = {};
                for (i in from) {
                    doppel[i] = recurse(from[i], doppel[i]);
                 }

            } else {
                doppel = from;
            }

            return doppel;
        };
    }());


    //  How to use to write plugins:
    //  BELT.extend(BELT.namespace('ns'), { props });
    //  jQuery(target, obj1)...
    //  BELT.extend();
    //  @dep isArray, isObject, isFunction, report, clone
    //  `object` is a crappy argument name.
    BELT.extend = BELT.core.extend = function(target, object, useHard) {
        var clone,
            extended,
            dex,
            key,
            new_key,
            len,
            i;

        //  By default, hard overwrite of target properties is disabled.
        useHard = useHard || false;


        //  If it's a function.
        //  Pass in the prototype, but return the function.
        BELT.core.isFunction(target) && 
            (extended = target, target = target.prototype);


        //  Error checking.
        //    ONLY check that target is a FN or object, and that 
        //    `object` is an object.  (might be array or obj).
        !BELT.core.isObject(target) && !BELT.core.isFunction(target) &&
            BELT.problem.report({});
        !BELT.core.isObject(object) && BELT.problem.report({});


        if (BELT.core.isArray(object)) {

            //  Merge Arrays if `target` and `object` are both arrays.
            //  This generally does a standard array merge:
            //
            //  a = [1,2] , b = [2,3], a merge b => [1,2,3]
            //  
            //  But if you have hard writing enabled, this will actually
            //  produce MESH instances of a and b into a with clones.
            //
            //  a = [{animal:panda}], b = [{animal:fox}].
            //  a merge b => [{animal:fox}].
            //  a[0].animal = "artic fox"; 
            //  a[0].animal => "artic fox", b[0].animal => "fox"
            //  
            //  Useful if you want to produce clones of stuff in b and
            //  mutate them safely elsewhere.
            //  
            if (BELT.core.isArray(target)) {
                console.log("Extending array with array...");
                len = object.length;
                for (i = 0; i < len; i += 1) {
                    if (useHard) {
                        if (target[i] === object[i]) { continue; }
                        target[i] = BELT.core.clone(object[i]);

                    } else {
                        //  Index of any matches.
                        dex = target.indexOf(object[i]);

                        //  If it's in there, continue.
                        //  If you don't want that, use concat().
                        if (dex > 0) { continue; }
                       
                        //  Add a clone into target.
                        target.push(BELT.core.clone(object[i]));
                    }
                }

                return target;

            } else {
                console.log("Extending object with array...");

                key = new_key = 'Array';
                i = 2;

                while (target[new_key] !== undefined) {
                    console.log('entering while loop');
                    if (BELT.core.areArraysEqual(target[new_key], object)) { 
                        return target; 
                    }
                    new_key = key + i;
                    i += 1;
                }

                target[new_key] = BELT.core.clone(object);
                return target;
            }

        } else if (BELT.core.isObject(object)) {
            console.log("We're extending target with an object.");
            console.log("This counts when you pass in a function or object as the target.");

            //  CAUTION:
            //  This will not match object-to-object equivalence.
            //  It will only capture whether an intance of what you've 
            //  passed in is already FOUND within the array, not if there
            //  is an object EQUAL TO the instance you're merging in.
            //
            //  Don't be surprised.
            //
            //  If you're looking for a strict test of whether objects are
            //  equal, _.isEqual() in the underscore library will do that.

            if (BELT.core.isArray(target)){
                console.log('Extending an array with an object...');
                console.warn('WARNING: see documentation for core.extend() for behavior.');
                console.warn('Currently, this functions more like push() than not.');

                if (target.indexOf(object) > -1) {
                    return target;
                }

                target.push(BELT.core.clone(object));
                return target;
            }
            else {
                console.log('Extending object with an object...');

                //  Mesh properties of object with target.
                //  This is a more traditional `extend` function.

            }
        }


        //  Output target.
        //console.error('WARNING: BELT.extend() incomplete.');
        return extended || target;
    };


    BELT.namespace = BELT.core.namespace = function(str) {
        var levels = str && str.split && str.split('.'),
            i = 0,
            name_space,
            step,
            msg,
            len;

        //  Fallthrough if incorrect usage.
        if (levels === undefined) {
            msg = 'BELT.namespace(str) : str can\'t be ' + str;
            throw new Error(msg);
        }

        //  Find the namespace (should be first str.)
        name_space = levels[0];
        step = global[levels[0]] || (global[levels[0]] = {});
        levels.shift();

        //  Iterate through the levels of the namespace string.
        len = levels.length;
        for (; i < len; i += 1) {
            if (typeof step[levels[i]] === 'undefined') {
                step[levels[i]] = {};

            } else if (typeof step[levels[i]] === 'function') {
                i += 1;
                msg = 'BELT.namespace() : ' + name_space + '.';
                msg += levels.slice(0, i).join('.') + ' is a function';
                throw new Error(msg);
            }

            step = step[levels[i]];
        }

        return step;
    };

    BELT.inherit = BELT.core.inherit = (function(){
        var F = function(){};
        return function inherit(P, C) {
            var opt = typeof P === 'object' ?
                        P :
                        { parent:P, child:C};

            F.prototype = opt.parent.prototype;
            opt.child.prototype = new F();
            opt.child.prototype.constructor = opt.child;
        };
    }());



    
    BELT.core.createConsoleAlias = function(){
       var new_console = new function BELTConsole() {},
            g_console = BELT.storage.get('global_console'),
            method_groups = {
                ie_opera_ff_webkit: ['log', 'warn', 'error', 'info', 'assert', 'clear'],
                opera_ff_webkit: ['dir', 'trace', 'profile', 'profileEnd', 'group', 'groupEnd'],
                ff_webkit: ['debug', 'groupCollapsed', 'time', 'timeEnd', 'noSuchMethod'],
                webkit: ['timeStamp', 'markTimeline', 'dirxml']
            },
            group,
            method,
            len,
            fn,
            i;

        for (group in method_groups) {
            len = method_groups[group].length;
            for (i = 0; i < len; i += 1) {
                new_console[method_groups[group][i]] = (function(method) { 
                    //  Must be called directly on console object, otherwise you
                    //    have IllegalInvocation errors.
                    if (g_console[method]) {
                        fn = function(a) { g_console[method](a) };
                    } else if (g_console['log']) {
                        fn = function(a) { g_console['log'](a) };
                    } else {
                        fn = function(){};
                    }
                    return fn;
                }(method_groups[group][i]));
            }
        }

        //  Allow direct access to private Webkit Console APIs that aren't methods.
        new_console.memory = g_console.memory || {};
        new_console.profiles = g_console.profiles || {};

        //  Return aliased functions.
        return new_console;
    };



/*
*   DATE
*
*   General purpose date handling module.
*
*   timestamp()
*       Returns a unix timestamp.
*
*/
BELT.date = {};

    BELT.timestamp = BELT.date.timestamp = function() {
        return Math.floor(Date.now() / 1000);
    };


/*
*   PROBLEM
*
*   Error handling for your JS app.
*
*   reportProblem()
*       Used for logging problems. ...?
*           @arguments : message [, type][, fn_name]
*                        { message:, [,type:] [,fn_name:] }
*           @return    :
*
*   Err
*       @dep       : Belt.date.timestamp
*       @arguments : message [, fn_name]
*                    { message: [, fn_name:] }
*/

BELT.problem = {};    // namespace.

    //  Public API.
    BELT.reportProblem = BELT.problem.report = function(message, type, fn_name) {
        var opt = (typeof message === 'object') ?
                  message :
                  {message: message, type: type, fn_name: fn_name};

        //  Mandatory Arg: message.
        opt.message === undefined &&
            BELT.problem.report({ /* !!! Error message API */ });

        //  Processing ...
        console.error('Error: BELT.problem.report() incomplete.');
    };


    //  ERRORS
    //  1.  Setting the prototype to an error instance directly does not help; the problem
    //      is that the trace starts with this file and not the actual location
    //      that the error happens.
    //  2.  This implementation of stack can also cause an enormous perf
    //      hit--up to 50% slower with stack evaluation inside BELT than 
    //      the native implementation.
    //
    //  HYPOTHESES
    //  1.  Removing `stack` call during instantiation will improve performance.
    //      > TRUE
    //  2.  Moving properties to a prototype will improve performance.
    //      > TRUE  2x - 4x gains.
    //      http://jsperf.com/belt-error-perf/4  ...   360,000 Safari
    //      http://jsperf.com/belt-error-perf/5  ... 1,499,000 Safari
    //
    //
    //  OBSERVATIONS
    //  1.  BELT.inherit() is slower with the direct-prototype inheritance.
    //      > 799,000 vs 812,000 OPS
    //  
    //
    BELT.problem.Error = function(message) {
        this.message = message || '';
        this._e = new Error();
        this.timestamp = BELT.date.timestamp();
    };
    BELT.problem.Error.prototype = new Error();
    BELT.problem.Error.prototype.constructor = BELT.problem.Error;
    
    BELT.problem.Error.prototype.stack = undefined;
    BELT.problem.Error.prototype.getStack = function(){ 
        this.stack = this.stack || this._e.stack;
        return this.stack;
    };
    BELT.problem.Error.prototype.navigator_raw = global.navigator || 'Unavailable.';
    BELT.problem.Error.prototype.location_raw = global.location || 'Unavailable.';







/*
*   STORAGE (CACHE)
*
*   storage.set()
*       Set a key-value pair in the private _cache object.
*           @arguments : key, value / {key:, value:}
*           @returns   : true if success
*
*   storage.get()
*       Retreive an item from the private _cache object.
*           @arguments : key [string] / [empty]
*           @return    : _cache.key / clone of _cache
*
*/
BELT.storage = {};
    BELT.storage.set = function(key, value) {
        var opt = (typeof key === 'object') ?
                    key :
                    {key: key, value: value};

        //  Arg checking: overkill, but better than losing data.
        opt.key === undefined &&
            BELT.problem.report({/* !!! Error message API */});
        typeof opt.key !== 'string' &&
            BELT.problem.report({ /* !!! Error message API */});
        opt.value === undefined &&
            BELT.problem.report({ /* !!! Error message API */ });

        //  Set to private cache.
        _cache[opt.key] = opt.value;
    };

    BELT.storage.get = function(key) {
        if (key) { return _cache[key] } 
        else { return BELT.core.clone(_cache) }
    };






/* ----------- bottom ------------- */




/*
*   EXPOSE/EXPORT
*
*   Create a safe console alias.
*
*   Expose local BELT variable to the global namespace, based on the
*     global variable passed into this self-invoking function.
*
*/
    global.console = BELT.core.createConsoleAlias();
    global.BELT = BELT;

}(window));  //   Will either be GLOBAL (node) || window (browser)

</script>

Setup

var 
        x = {
            a: [],
            b: {
                one: "one",
                two: {
                  point1: 2.1,
                  point2: 2.2,
                  point3: 2.3
                },
                three: [3.1, 3.2, 3.3, 3.4]
            },
            c: function(b) { b = 1; return b },
            d: "okay",
            e: 1
        },
        x2 = {
            a: [],
            b: {
                one: "one",
                two: {
                  point1: 2.1,
                  point2: 2.2,
                  point3: 2.3
                },
                three: [3.1, 3.2, 3.3, 3.4]
            },
            c: function(b) { b = 1; return b },
            d: "okay",
            e: 1
        },
    
        y = {
            a: [],
            b: {
                one: "one",
                two: {
                  point1: 2.1,
                  point2: 2.2,
                  point3: 2.3
                },
                three: [3.1, 3.2, 3.3, 3.4]
            },
            c: function(b) { b = 1; return b },
            d: "okay",
            e: 2
        },
    
        z = {
            a: [],
            b: {
                one: "one",
                two: {
                  point1: 2.1,
                  point2: 2.2,
                  point3: 2.3
                },
                three: [3.1, 3.2, 3.3, 3.4]
            },
            c: function(b) { b = 2; return b },
            d: "okay",
            e: 2
        };

Test runner

Ready to run.

Testing in
TestOps/sec
Belt.core.isEqual (d4)
BELT.core.isEqual_d4(x, x2);
BELT.core.isEqual_d4(x, y);
BELT.core.isEqual_d4(x, z);
ready
_.isEqual
_.isEqual(x, x2);
_.isEqual(x, y);
_.isEqual(x, z);
ready
BELT.core.isEqual (d6)
BELT.core.isEqual_d6(x, x2);
BELT.core.isEqual_d6(x, y);
BELT.core.isEqual_d6(x, z);
ready
BELT.core.isEqual (d7)
BELT.core.isEqual_d7(x, x2);
BELT.core.isEqual_d7(x, y);
BELT.core.isEqual_d7(x, z);
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.

  • Revision 12: published by John Cockrell on