Skip to content

Instantly share code, notes, and snippets.

@datchley
Last active December 25, 2015 12:59
Show Gist options
  • Select an option

  • Save datchley/6980113 to your computer and use it in GitHub Desktop.

Select an option

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…
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));
<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