Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid stack overflow when comparing objects with cycles (like DOM Elements) #5

Open
wants to merge 7 commits into
base: gh-pages
Choose a base branch
from
92 changes: 74 additions & 18 deletions objectDiff.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,28 @@ var objectDiff = typeof exports != 'undefined' ? exports : {};
* @param {Object} b
* @return {Object}
*/
objectDiff.diff = function diff(a, b) {
objectDiff.diff = function diff(a, b, ignore) {

if (a === b) {
return {
changed: 'equal',
value: a
}
}
if (a.__diffComparedTo__ === b && b.__diffComparedTo__ === a) {
return true;
}

var value = {};
var equal = true;
var ignoredKeys = ['__diffComparedTo__'].concat(ignore || []);

a.__diffComparedTo__ = b;
b.__diffComparedTo__ = a;

for (var key in a) {
if (ignoredKeys.indexOf(key) != -1) { continue; }

if (key in b) {
if (a[key] === b[key]) {
value[key] = {
Expand All @@ -28,7 +37,7 @@ objectDiff.diff = function diff(a, b) {
var typeA = typeof a[key];
var typeB = typeof b[key];
if (a[key] && b[key] && (typeA == 'object' || typeA == 'function') && (typeB == 'object' || typeB == 'function')) {
var valueDiff = diff(a[key], b[key]);
var valueDiff = diff(a[key], b[key], ignore);
if (valueDiff.changed == 'equal') {
value[key] = {
changed: 'equal',
Expand Down Expand Up @@ -57,6 +66,8 @@ objectDiff.diff = function diff(a, b) {
}

for (key in b) {
if (ignoredKeys.indexOf(key) != -1) { continue; }

if (!(key in a)) {
equal = false;
value[key] = {
Expand All @@ -66,6 +77,9 @@ objectDiff.diff = function diff(a, b) {
}
}

delete a.__diffComparedTo__;
delete b.__diffComparedTo__;

if (equal) {
return {
changed: 'equal',
Expand All @@ -85,21 +99,33 @@ objectDiff.diff = function diff(a, b) {
* @param {Object} b
* @return {Object}
*/
objectDiff.diffOwnProperties = function diffOwnProperties(a, b) {
objectDiff.diffOwnProperties = function diffOwnProperties(a, b, ignore) {

if (a === b) {
return {
changed: 'equal',
value: a
}
}
if (a && b && a.__diffComparedTo__ === b && b.__diffComparedTo__ === a) {
return true;
}

var diff = {};
var equal = true;
var keys = Object.keys(a);
var typeofA = typeof a;
var typeofB = typeof b;
var keys = [];
var ignoredKeys = ['__diffComparedTo__'].concat(ignore || []);

if (a && typeofA === 'object') { keys = Object.keys(a); }
if (a) { a.__diffComparedTo__ = b; }
if (b) { b.__diffComparedTo__ = a; }

for (var i = 0, length = keys.length; i < length; i++) {
var key = keys[i];
if (ignoredKeys.indexOf(key) != -1) { continue; }

if (b.hasOwnProperty(key)) {
if (a[key] === b[key]) {
diff[key] = {
Expand All @@ -110,7 +136,7 @@ objectDiff.diffOwnProperties = function diffOwnProperties(a, b) {
var typeA = typeof a[key];
var typeB = typeof b[key];
if (a[key] && b[key] && (typeA == 'object' || typeA == 'function') && (typeB == 'object' || typeB == 'function')) {
var valueDiff = diffOwnProperties(a[key], b[key]);
var valueDiff = diffOwnProperties(a[key], b[key], ignore);
if (valueDiff.changed == 'equal') {
diff[key] = {
changed: 'equal',
Expand Down Expand Up @@ -138,10 +164,13 @@ objectDiff.diffOwnProperties = function diffOwnProperties(a, b) {
}
}

keys = Object.keys(b);
keys = [];
if (b && typeofB === 'object') { keys = Object.keys(b); }

for (i = 0, length = keys.length; i < length; i++) {
key = keys[i];
if (ignoredKeys.indexOf(key) != -1) { continue; }

if (!a.hasOwnProperty(key)) {
equal = false;
diff[key] = {
Expand All @@ -151,11 +180,20 @@ objectDiff.diffOwnProperties = function diffOwnProperties(a, b) {
}
}

if (equal) {
if (a) { delete a.__diffComparedTo__; }
if (b) { delete b.__diffComparedTo__; }

if (equal && (typeofA === typeofB) && (typeofA === 'object')) {
return {
value: a,
changed: 'equal'
}
} else if (equal) {
return {
changed: 'primitive change',
removed: a,
added: b
}
} else {
return {
changed: 'object change',
Expand All @@ -177,6 +215,14 @@ objectDiff.diffOwnProperties = function diffOwnProperties(a, b) {
var diff = changes.value;
if (changes.changed == 'equal') {
return inspect(diff);
} else if (changes.changed == 'primitive change') {
return [
'<div class="diff-level">',
'<del class="diff diff-key">', inspect(changes.removed), '</del>',
'<span>,</span>\n',
'<ins class="diff diff-key">', inspect(changes.added), '</ins>',
'</div>'
].join('');
}

for (var key in diff) {
Expand Down Expand Up @@ -215,9 +261,7 @@ objectDiff.diffOwnProperties = function diffOwnProperties(a, b) {
* @return {string}
*/
function stringifyObjectKey(key) {
return /^[a-z0-9_$]*$/i.test(key) ?
key :
JSON.stringify(key);
return /^[a-z0-9_$]*$/i.test(key) ? key : JSON.stringify(key);
}

/**
Expand All @@ -242,7 +286,8 @@ objectDiff.diffOwnProperties = function diffOwnProperties(a, b) {
* @see http://jsperf.com/continuation-passing-style/3
* @return {string}
*/
function _inspect(accumulator, obj) {
function _inspect(accumulator, obj, deep) {
deep = deep || 0;
switch(typeof obj) {
case 'object':
if (!obj) {
Expand All @@ -253,23 +298,34 @@ objectDiff.diffOwnProperties = function diffOwnProperties(a, b) {
var length = keys.length;
if (length === 0) {
accumulator += '<span>{}</span>';
} else if (obj.nodeType > 0) {
accumulator += '<span>' + Object.prototype.toString.call(obj) + '</span>';
} else {
accumulator += '<span>{</span>\n<div class="diff-level">';
for (var i = 0; i < length; i++) {
var key = keys[i];
accumulator = _inspect(accumulator + stringifyObjectKey(escapeHTML(key)) + '<span>: </span>', obj[key]);
if (i < length - 1) {
accumulator += '<span>,</span>\n';
deep += 1;
if (deep > 3) {
accumulator += '<span>[too deep...]</span>';
} else {
accumulator += '<span>{</span>\n<div class="diff-level">';
for (var i = 0; i < length; i++) {
var key = keys[i];
accumulator = _inspect(accumulator + stringifyObjectKey(escapeHTML(key)) + '<span>: </span>', obj[key], deep);
if (i < length - 1) {
accumulator += '<span>,</span>\n';
}
}
accumulator += '\n</div><span>}</span>'
}
accumulator += '\n</div><span>}</span>'
}
break;

case 'string':
accumulator += JSON.stringify(escapeHTML(obj));
break;

case 'function':
accumulator += Object.prototype.toString.call(obj);
break;

case 'undefined':
accumulator += 'undefined';
break;
Expand Down