JavaScript’s abstract (==) and strict (===) equality operators work fine for scalars like 1, 2, and ‘2’:

1 == 2     || false
2 == '2'   && true
2 === '2'  || false

Objects are another story. Two distinct objects are never equal for either strictly or abstract comparisons. An expression comparing objects is only true if the operands reference the same object. In other words, JavaScript’s equality operators applied to objects only compare their references. That is not very useful.

So I wrote a jQuery plugin to fix that issue.

$.equals = function( a, b, options ) {

    options = $.extend({

        // if true, in case of inequality, a trace of the first difference is logged to the console
        verbose: false,

        // use 'strict' for ===, 'abstract' for ==
        comparison: 'strict',

        // if true, an item is compared, otherwise it is ignored
        // receives a value and a key, returns true or false
        filter: function () {
            return true;
        }

    }, options || {});

    var initial_step = false;
    if (typeof options.trace == 'undefined') {
        initial_step = true;
        options.trace = [];
    }

    if (options.comparison == 'strict' ? a === b : a == b) {
        return true;
    }

    var a_type = $.type(a);
    var b_type = $.type(b);
    if (! (a_type == b_type)) {
        log(['types', a_type, b_type, a, b]);
        return false;
    }

    var a_keys = decompose(a, options.filter).keys;
    var b_keys = decompose(b, options.filter).keys;

    var a_length = a_keys.length;
    var b_length = b_keys.length;
    if (! (a_length == b_length)) {
        log(['lengths', a_length, b_length, a, b]);
        return false;
    }

    var length = a_length; // == b_length
    if (length == 0) { // a and b are scalars
        return a === b;
    }

    // compare corresponding elements
    for (var i = 0; i < length; i++) {
        var key = a_keys[i];
        var a_value = a[key];
        var b_value = b[key];
        var equal = $.equals( a_value, b_value, options );
        if (! equal) {
            log(['values at key "' + key + '"', a_value, b_value, a, b]);
            return false;
        }
    }

    if (initial_step) {
        log(['values', null, null, a, b]);
    }
    return true;

    //---

    // returns true if the given object is a scalar (not in JavaScript terms, though...)
    function isScalarValue(object) {
        return object === null || /undefined|boolean|number|string/.test(typeof object);
    }

    // returns a plain object with the given object's items correspondingly split into keys and values arrays
    function decompose(object, filter) {
        if (isScalarValue(object)) {
            return {
                keys: [],
                values: filter(object) ? object : null
            };
        }
        var keys = [];
        var values = [];
        for (var key in object) {
            var value = object[key];
            if (filter(value, key)) {
                keys.push(key);
                values.push(value);
            }
        }
        return {keys: keys, values: values};
    }

    // returns a plain object with the given decomposed's keys and values arrays correspondingly joined into items
    // note that
    //     (1) compose(decompose(plain_object)) == plain_object
    //     (2) compose(decompose(non_plain_object)) != non_plain_object
    function compose(decomposed) {
        var result = {};
        for (var i = 0, iTop = decomposed.keys.length; i < iTop; i++) {
            result[decomposed.keys[i]] = decomposed.values[i];
        }
        return result;
    }

    // logs given data to the console if it's the initial_step log, otherwise it only remembers
    function log(data) {
        if (! options.verbose) return;
        options.trace.push(data);
        if (initial_step) {
            console_log('');
            while (options.trace.length) {
                console_log('  ');
            }
        }

        //---

        function console_log(prefix) {
            var top = options.trace.pop();
            var data = compose({keys: ['difference', 'a_attribute', 'b_attribute', 'a', 'b'], values: top});
            if ('values' == data.difference) {
                console.log(' ($.equals)', prefix + 'comparing(', data.a, ', ', data.b, ')\n',
                    '($.equals)', prefix + '    found different values');
            }
            else {
                console.log(' ($.equals)', prefix + 'comparing(', data.a, ', ', data.b, ')\n',
                    '($.equals)', prefix + '    found different', data.difference, data.a_attribute, '!=', data.b_attribute);
            }
        }
    }
};

Here are some simple use cases

var count = 0;

console.log('--\n Case ' + (++count) + ': two undefined values');
console.log($.equals(undefined, undefined, {verbose: true}));

console.log('--\n Case ' + (++count) + ': two null values');
console.log($.equals(null, null, {verbose: true}));

console.log('--\n Case ' + (++count) + ': undefined and null values');
console.log($.equals(undefined, null, {verbose: true}));

console.log('--\n Case ' + (++count) + ': undefined and null values -- abstract comparison');
console.log($.equals(undefined, null, {verbose: true, comparison: 'abstract'}));

console.log('--\n Case ' + (++count) + ': two NaN values');
console.log($.equals(NaN, NaN, {verbose: true}));

console.log('--\n Case ' + (++count) + ': two different true values');
console.log($.equals(1, true, {verbose: true}));

console.log('--\n Case ' + (++count) + ': two different false values -- abstract comparison');
console.log($.equals(0, false, {verbose: true, comparison: 'abstract'}));

console.log('--\n Case ' + (++count) + ': same integer 123, 123');
console.log($.equals(123, 123, {verbose: true}));

console.log('--\n Case ' + (++count) + ': same number 123, Number(123)');
console.log($.equals(123, Number(123), {verbose: true}));

console.log('--\n Case ' + (++count) + ': same numeric value 123, 123.0');
console.log($.equals(123, 123.0, {verbose: true}));

console.log('--\n Case ' + (++count) + ': same numeric value 123, "123"');
console.log($.equals(123, "123", {verbose: true}));

console.log('--\n Case ' + (++count) + ': same numeric value 123, "123" -- abstract comparison');
console.log($.equals(123, "123", {verbose: true, comparison: 'abstract'}));

console.log('--\n Case ' + (++count) + ': two different strings');
console.log($.equals('a string', 'another string', {verbose: true}));

console.log('--\n Case ' + (++count) + ': two variables referencing the same object');
var test1 = {a: 1, b: 2}, test2 = test1;
console.log($.equals(test1, test2, {verbose: true}));

console.log('--\n Case ' + (++count) + ': an object copy and pasted from another one');
var test1 = {a: 1, b: 2}, test2 = {a: 1, b: 2};
console.log($.equals(test1, test2, {verbose: true}));

console.log('--\n Case ' + (++count) + ': two objects with the same keys and values, but in a different order');
var test1 = {a: 1, b: 2}, test2 = {b: 2, a: 1};
console.log($.equals(test1, test2, {verbose: true}));

console.log('--\n Case ' + (++count) + ': two objects with the same keys and values, but one different value');
var test1 = {a: 1, b: 2}, test2 = {b: 2, a: '1'};
console.log($.equals(test1, test2, {verbose: true}));

console.log('--\n Case ' + (++count) + ': two objects with the same keys and values, but one different value -- abstract comparison');
var test1 = {a: 1, b: 2}, test2 = {b: 2, a: '1'};
console.log($.equals(test1, test2, {verbose: true, comparison: 'abstract'}));

whose console output is

--
 Case 1: two undefined values
true
--
 Case 2: two null values
true
--
 Case 3: undefined and null values
 ($.equals) comparing( undefined ,  null )
 ($.equals)     found different types undefined != null
false
--
 Case 4: undefined and null values -- abstract comparison
true
--
 Case 5: two NaN values
false
--
 Case 6: two different true values
 ($.equals) comparing( 1 ,  true )
 ($.equals)     found different types number != boolean
false
--
 Case 7: two different false values -- abstract comparison
true
--
 Case 8: same integer 123, 123
true
--
 Case 9: same number 123, Number(123)
true
--
 Case 10: same numeric value 123, 123.0
true
--
 Case 11: same numeric value 123, "123"
 ($.equals) comparing( 123 ,  123 )
 ($.equals)     found different types number != string
false
--
 Case 12: same numeric value 123, "123" -- abstract comparison
true
--
 Case 13: two different strings
false
--
 Case 14: two variables referencing the same object
true
--
 Case 15: an object copy and pasted from another one
 ($.equals) comparing( Object {a: 1, b: 2} ,  Object {a: 1, b: 2} )
 ($.equals)     found different values
true
--
 Case 16: two objects with the same keys and values, but in a different order
 ($.equals) comparing( Object {a: 1, b: 2} ,  Object {b: 2, a: 1} )
 ($.equals)     found different values
true
--
 Case 17: two objects with the same keys and values, but one different value
 ($.equals) comparing( Object {a: 1, b: 2} ,  Object {b: 2, a: "1"} )
 ($.equals)     found different values at key "a" 1 != 1
 ($.equals)   comparing( 1 ,  1 )
 ($.equals)       found different types number != string
false
--
 Case 18: two objects with the same keys and values, but one different value -- abstract comparison
 ($.equals) comparing( Object {a: 1, b: 2} ,  Object {b: 2, a: "1"} )
 ($.equals)     found different values
true

Advanced usage

That’s all well and fun, but comparing non-plain objects is where $.equals shines.

With that in mind, I wrote two symmetrical operations for jQuery: scatter and gather. In Maths multiplication distributes over addition, allowing you to scatter a x (b + c) to a x b + a x c, and gather back a x b + a x c to a x (b + c). Likewise with these two operations, you can scatter jQuery over its elements: $:[ e1, e2 ] to [ $:e1, $:e2 ] and gather back [ $:e1, $:e2 ] to $:[ e1, e2 ].

//scatter( $:[ e1, e2, ... ] ) == [ $:e1, $:e2, ... ]
$.fn.scatter = function () {
    return $.map(this, function (el) { return $(el); });
};

//gather( [ $:e1, $:e2, ... ] ) == $:[ e1, e2, ... ]
$.gather = function(array) {
    return $($.map(array, function ($el) { return $el.get(); }));
};

Here are some advanced use cases

var object_$ = $('*');
var array_$_toArray = object_$.toArray();
var object_$_toArray = $(array_$_toArray);
var array_$_scatter = object_$.scatter();
var object_gather_$_scatter = $.gather(array_$_scatter);

function isDomElement(item) {
    return (item instanceof HTMLElement);
}

console.log('--\n Case ' + (++count) + ': object_$, $(object_$.toArray())');
console.log($.equals(object_$, object_$_toArray, {verbose: true}));

console.log('--\n Case ' + (++count) + ': object_$, $(object_$.toArray()) -- DOM Elements only');
console.log($.equals(object_$, object_$_toArray, {verbose: true, filter: isDomElement}));

console.log('--\n Case ' + (++count) + ': object_$, $.gather(object_$.scatter())');
console.log($.equals(object_$, object_gather_$_scatter, {verbose: true}));

console.log('--\n Case ' + (++count) + ': object_$, $.gather(object_$.scatter()) -- DOM Elements only');
console.log($.equals(object_$, object_gather_$_scatter, {verbose: true, filter: isDomElement}));

console.log('--\n Case ' + (++count) + ': $(object_$.toArray()), $.gather(object_$.scatter())');
console.log($.equals(object_$_toArray, object_gather_$_scatter, {verbose: true}));

console.log('--\n Case ' + (++count) + ': $("body"), $("head") -- DOM Elements only');
console.log($.equals($("body"), $("head"), {verbose: true, filter: isDomElement}));

whose console output is

--
 Case 19: object_$, $(object_$.toArray())
 ($.equals) comparing( 
[html, head, meta, title, script, link, style, script, body, p, div, span, prevObject: jQuery.fn.init[1], context: document, selector: "*", jquery: "1.11.2-pre ... ]
 ,  
[html, head, meta, title, script, link, style, script, body, p, div, span, jquery: "1.11.2-pre ... ]
 )
 ($.equals)     found different lengths 163 != 161
false
--
 Case 20: object_$, $(object_$.toArray()) -- DOM Elements only
 ($.equals) comparing( 
[html, head, meta, title, script, link, style, script, body, p, div, span, prevObject: jQuery.fn.init[1], context: document, selector: "*", jquery: "1.11.2-pre ... ]
 ,  
[html, head, meta, title, script, link, style, script, body, p, div, span, jquery: "1.11.2-pre ... ]
 )
 ($.equals)     found different values
true
--
 Case 21: object_$, $.gather(object_$.scatter())
 ($.equals) comparing( 
[html, head, meta, title, script, link, style, script, body, p, div, span, prevObject: jQuery.fn.init[1], context: document, selector: "*", jquery: "1.11.2-pre ... ]
 ,  
[html, head, meta, title, script, link, style, script, body, p, div, span, jquery: "1.11.2-pre ... ]
 )
 ($.equals)     found different lengths 163 != 161
false
--
 Case 22: object_$, $.gather(object_$.scatter()) -- DOM Elements only
 ($.equals) comparing( 
[html, head, meta, title, script, link, style, script, body, p, div, span, prevObject: jQuery.fn.init[1], context: document, selector: "*", jquery: "1.11.2-pre ... ]
 ,  
[html, head, meta, title, script, link, style, script, body, p, div, span, jquery: "1.11.2-pre ... ]
 )
 ($.equals)     found different values
true
--
 Case 23: $(object_$.toArray()), $.gather(object_$.scatter())
 ($.equals) comparing( 
[html, head, meta, title, script, link, style, script, body, p, div, span, jquery: "1.11.2-pre ... ]
 ,  
[html, head, meta, title, script, link, style, script, body, p, div, span, jquery: "1.11.2-pre ... ]
 )
 ($.equals)     found different values
true
--
 Case 24: $("body"), $("head") -- DOM Elements only
 ($.equals) comparing( 
[body, prevObject: jQuery.fn.init[1], context: document, selector: "body", jquery: "1.11.2-pre ... ]
 ,  
[head, prevObject: jQuery.fn.init[1], context: document, selector: "head", jquery: "1.11.2-pre ... ]
 )
 ($.equals)     found different values at key "0" <body>​…​</body>​ != <head>​…​</head>​
 ($.equals)   comparing( <body>​…​</body>​ ,  <head>​…​</head>​ )
 ($.equals)       found different values at key "lastElementChild" <div>​…​</div>​ != <script type=​"text/​javascript">​…​</script>​
 ($.equals)   comparing( <div>​…​</div>​ ,  <script type=​"text/​javascript">​…​</script>​ )
 ($.equals)       found different lengths 6 != 3
false

As you see, even if by looking at the console you can easily spot outstanding differences thanks to the great object inspection offered by the browser, my plugin is dumber by design, and in general it finds different differences. The important thing is that you get a false or a true when you really expect a false or a true, respectively.

In particular,

  • case 19 and 20 tell us that object_$ and $(object_$.toArray()) are not identical but they contain the same DOM elements
  • case 21 and 22 tell us that object_$ and $.gather(object_$.scatter()) are not identical but they contain the same DOM elements
  • case 23 tells us that $(object_$.toArray()) and $.gather(object_$.scatter()) are identical (but still different objects)
  • case 24 tells us that $(“body”) and $(“head”) are different… as expected :-)
    • please notice that they are both jQuery objects containing only one DOM element, but given the way the filter is written, $.equals tries to compare also lastElementChild. We could have taken the key into consideration like filter: function(value, key) { … }

Here is a jsFiddle if you want to play around.