Last active
December 25, 2015 12:59
-
-
Save datchley/6980113 to your computer and use it in GitHub Desktop.
The ability to do a deep copy of objects and arrays in javascript, handling Dates, RegEx and others correctly; along with a deep equals for comparing javascript objects. Both functions handle native types as well as objects and arrays; and the deep equals function should treat arrays with the same values; but not in the same order as equal as we…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| if (!Array.isArray) { | |
| Array.isArray = function(a) { return Object.prototype.toString.call(a) == '[object Array]'; } | |
| } | |
| if (!Object.isObject) { | |
| Object.isObject = function(a) { return Object.prototype.toString.call(a) == '[object Object]'; } | |
| } | |
| // Clone an object or native type in javascript | |
| // Exceptions (doesn't handle these types): | |
| // (1) DOM nodes - use native cloneNode() | |
| // (3) non-plain objects (ie. custom objects with special constructors) | |
| // (4) does not copy functions/methods | |
| // (5) does not support all HTML5 types | |
| function clone(src, srcStack, destStack) { | |
| var type = toString.call(src); | |
| if (src === null || !(src instanceof Object)) | |
| return src; | |
| if (type == '[object String]' || type == '[object Number]' || type == '[object Boolean]') | |
| return src; | |
| if (type == '[object Date]') | |
| return new Date(src.getTime()); | |
| if (type == '[object RegExp]') { | |
| var flags = (src.multiline ? 'm' : '') | |
| + (src.ignoreCase ? 'i' : '') | |
| + (src.global ? 'g' : ''), | |
| _rx = new RegExp(src.source, flags); | |
| return _rx; | |
| } | |
| var dest; | |
| // Keep track of copies of nested objects to avoid circular references | |
| srcStack || (srcStack = [], destStack = []); | |
| var length = srcStack.length; | |
| while (length--) { | |
| if (srcStack[length] == src) { | |
| // return the previous copy if we've seen this object before | |
| return destStack[length]; | |
| } | |
| } | |
| srcStack.push(src); | |
| if (type == '[object Array]') { | |
| var _dest = [], | |
| size = src.length; | |
| destStack.push(_dest); | |
| for (var i=0; i<size; i++) { | |
| if (i in src) { | |
| _dest.push(clone(src[i], srcStack, destStack)); | |
| } | |
| } | |
| return _dest; | |
| } | |
| if (type == '[object Object]') { | |
| var _dest = src.constructor ? new src.constructor : {}; | |
| destStack.push(_dest); | |
| for (var key in src) { | |
| if (src.hasOwnProperty(key)) { | |
| _dest[key] = clone(src[key], srcStack, destStack); | |
| } | |
| } | |
| return _dest; | |
| } | |
| // just return the value | |
| return src; | |
| }; | |
| // Implementation heavily borrowed from underscore.js | |
| function equal(a, b, aStack, bStack) { | |
| var type = toString.call(a); | |
| if (a === b) | |
| return a !== 0 || 1/a == 1/b; | |
| if (a == null || b == null) | |
| return a === b; | |
| if (type != toString.call(b)) | |
| return false; | |
| switch(type) { | |
| // Strings, numbers, dates, and booleans are compared by value. | |
| case '[object String]': | |
| // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is | |
| // equivalent to `new String("5")`. | |
| return a == String(b); | |
| case '[object Number]': | |
| // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for | |
| // other numeric values. | |
| return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); | |
| case '[object Date]': | |
| case '[object Boolean]': | |
| // Coerce dates and booleans to numeric primitive values. Dates are compared by their | |
| // millisecond representations. Note that invalid dates with millisecond representations | |
| // of `NaN` are not equivalent. | |
| return +a == +b; | |
| // RegExps are compared by their source patterns and flags. | |
| case '[object RegExp]': | |
| return a.source == b.source && | |
| a.global == b.global && | |
| a.multiline == b.multiline && | |
| a.ignoreCase == b.ignoreCase; | |
| } | |
| // Keep track of copies of nested objects to avoid circular references | |
| aStack || (aStack = [], bStack = []); | |
| var length = aStack.length; | |
| while (length--) { | |
| if (aStack[length] == a) { | |
| return bStack[length] == b; | |
| } | |
| } | |
| aStack.push(a); | |
| bStack.push(b); | |
| if (type == '[object Object]') { | |
| var size = 0, | |
| result = true; | |
| for (var key in a) { | |
| if (a.hasOwnProperty(key)) { | |
| size++; | |
| result = b.hasOwnProperty(key) && equal(a[key], b[key], aStack, bStack); | |
| if (!result) break; | |
| } | |
| } | |
| if (result) { | |
| for (key in b) { | |
| if (b.hasOwnProperty(key) && !(size--)) { | |
| break; | |
| } | |
| } | |
| result = !size; | |
| } | |
| } | |
| if (type == '[object Array]') { | |
| var size = a.length, | |
| result = size == b.length, | |
| // out of order arrays with same values should be == | |
| // clone them so we don't modify originals | |
| _a = a.slice(0).sort(), | |
| _b = b.slice(0).sort(); | |
| if (result) { | |
| // do deeper comparison | |
| while (size--) { | |
| result = equal(_a[size], _b[size], aStack, bStack); | |
| if (!result) break; | |
| } | |
| } | |
| } | |
| aStack.pop(a); | |
| bStack.pop(b); | |
| return result; | |
| }; | |
| // Basic Type comparison | |
| console.log('"s" == "s"? ', equal("s", "s")); | |
| console.log('"s" == "t"? ', equal("s", "t")); | |
| console.log("5 == 5? ", equal(5, 5)); | |
| console.log("0 == 0? ", equal(0, 0)); | |
| console.log("0 == -0? ", equal(0, -0)); | |
| console.log("null == undefined? ", equal(null, undefined)); | |
| console.log("null == null? ", equal(null, null)); | |
| console.log("undefined == undefined? ", equal(undefined, undefined)); | |
| // Object comparisons | |
| var a = { a: 1, b: 2 }, | |
| b = { a: 1, b: 2 }, | |
| c = { a: 2, b: 2 }, | |
| arev = { b: 2, a: 1 }; | |
| console.log("{a} == {b}? ", equal(a, b)); | |
| console.log("{a} == {c}? ", equal(a, c)); | |
| console.log("{a} == {arev}? ", equal(a, arev)); | |
| // Array comparison | |
| var d = [1,2,3], | |
| drev = [3,2,1], | |
| e = [1,2], | |
| f = [1,2,3]; | |
| console.log("[d] == [drev]? ", equal(d, drev)); | |
| console.log("[e] == [f]? ", equal(e, f)); | |
| // Poll a data object, performing dirty checking and have it run | |
| // a callback function to perform an action | |
| var watch = (function() { | |
| var prevstate = undefined, | |
| DIRTY_CYCLE = 100; | |
| return function watch(obj, callback) { | |
| var dirty = !equal(prevstate, obj); | |
| //console.log("comparing (prev)%o with (current) %o", prevstate, obj); | |
| if (dirty) { | |
| prevstate = $.extend(true, {},obj); | |
| callback.call(obj); | |
| } | |
| setTimeout(function() { watch(obj, callback); }, DIRTY_CYCLE); | |
| }; | |
| })(); | |
| // The data to watch.... | |
| var watched = { | |
| name: 'David Atchley', | |
| age: 38, | |
| foods: [ 'pizza', 'olives'] | |
| }; | |
| // Setup the watch on the above data | |
| $(document).ready(function(){ | |
| watch(watched, function() { | |
| var len = this.foods.length; | |
| $('#name').text(this.name); | |
| $('#input-name').val(this.name); | |
| $('#age').text(this.age); | |
| $('#foods').empty(); | |
| while (len--) { | |
| $('#foods').append('<li>' + this.foods[len] + '</li>'); | |
| } | |
| console.log("object changed: %o", this); | |
| }); | |
| $('#input-name').on('change keyup', function(ev) { | |
| watched.name = $(this).val(); | |
| }); | |
| $('#btn-add-food').on('click', function(ev) { | |
| var val = $('#input-food').val() || ""; | |
| if (val != "" && typeof val !== 'undefined') { | |
| watched.foods.push($('#input-food').val()); | |
| $('#input-food').val(''); | |
| } | |
| }); | |
| }); | |
| // Cloning tests, individual and nested structures | |
| var tries = [ | |
| 1, | |
| "text", | |
| true, | |
| /^abc-.*/, | |
| new Date(Date.now()), | |
| ['a','b','c'], | |
| { | |
| a: 1, | |
| b: 2, | |
| c: 3 | |
| }], | |
| nested = { | |
| 'number': 1, | |
| 'char': "a string", | |
| 'bool': true, | |
| 'regex': /^(abc|pdf).*/g, | |
| 'now': new Date(Date.now()), | |
| 'list': [ 'cat', 'dog', 'hamster'], | |
| 'obj': { | |
| 'city': 'Washington', | |
| 'state': 'MO' | |
| } | |
| }; | |
| for (var index in tries) { | |
| console.log("Cloning %s to %o", toString.call(tries[index]), clone(tries[index])); | |
| } | |
| console.log("Cloning nested object to %o", clone(nested)); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <html> | |
| <head> | |
| <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script> | |
| </head> | |
| <body> | |
| <h1>What are we now?</h1> | |
| <div id="output"> | |
| <b>Name:</b> <span id="name"></span><br/> | |
| <b>Age:</b> <span id="age"></span><br/> | |
| <b>Foods:</b><br/> | |
| <ul id="foods"> | |
| </ul> | |
| <b>Edit</b><br/> | |
| Name: <input id="input-name" type="text" /><br/> | |
| Add Food? <input id="input-food" type="text" /><button id="btn-add-food">Add</button><br/> | |
| </div> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment