diff --git a/src/stringify.js b/src/stringify.js index 2c1a343c3701..8506f37bb624 100644 --- a/src/stringify.js +++ b/src/stringify.js @@ -3,18 +3,7 @@ /* global: toDebugString: true */ function serializeObject(obj) { - var seen = []; - - return JSON.stringify(obj, function(key, val) { - val = toJsonReplacer(key, val); - if (isObject(val)) { - - if (seen.indexOf(val) >= 0) return '<>'; - - seen.push(val); - } - return val; - }); + return JSON.stringify(decycleObject(obj), toJsonReplacer); } function toDebugString(obj) { @@ -27,3 +16,55 @@ function toDebugString(obj) { } return obj; } + +/** + * Loops through object properties and detects circular references. + * Detected circular references are replaced with '...'. + * + * @param {Object} object Object instance + * @param {Array=} seen Private argument, leave it undefined (it is used internally for recursion) + * @returns {Object} Simple representation of an object (plain object or array) + */ +function decycleObject(object, seen) { + // make sure simple types are returned untouched + if (!canContainCircularReference(object)) return object; + + // make sure to assign correct type of a safe object + var safeObject = isArray(object) ? [] : {}; + + // make local copy of the reference array to be sure + // objects are referenced in straight line + seen = seen ? seen.slice() : []; + + for (var key in object) { + var property = object[key]; + + if (canContainCircularReference(property)) { + if (seen.indexOf(property) >= 0) { + safeObject[key] = '...'; + } else { + if (seen.indexOf(object) === -1) seen.push(object); + safeObject[key] = decycleObject(property, seen); + } + } else { + safeObject[key] = property; + } + } + + return safeObject; +} + +/** + * Check if passed object is an enumerable object and has at least one key + * + * @param {Object} object + * @returns {Boolean} + */ +function canContainCircularReference(object) { + if (isObject(object)) { + for (var i in object) { + return true; + } + } + return false; +} diff --git a/test/minErrSpec.js b/test/minErrSpec.js index 848188c98ab0..bf1af6d60eea 100644 --- a/test/minErrSpec.js +++ b/test/minErrSpec.js @@ -65,7 +65,7 @@ describe('minErr', function() { a.b.a = a; var myError = testError('26', 'a is {0}', a); - expect(myError.message).toMatch(/a is {"b":{"a":"<>"}}/); + expect(myError.message).toMatch(/a is {"b":{"a":"..."}}/); }); it('should preserve interpolation markers when fewer arguments than needed are provided', function() { diff --git a/test/stringifySpec.js b/test/stringifySpec.js index e849b3e86cb8..632cc128af20 100644 --- a/test/stringifySpec.js +++ b/test/stringifySpec.js @@ -7,9 +7,11 @@ describe('toDebugString', function() { expect(toDebugString({a:{b:'c'}})).toEqual('{"a":{"b":"c"}}'); expect(toDebugString(function fn() { var a = 10; })).toEqual('function fn()'); expect(toDebugString()).toEqual('undefined'); - var a = { }; + + // circular references + var a = {}; a.a = a; - expect(toDebugString(a)).toEqual('{"a":"<>"}'); - expect(toDebugString([a,a])).toEqual('[{"a":"<>"},"<>"]'); + expect(toDebugString(a)).toEqual('{"a":{"a":"..."}}'); + expect(toDebugString([a,a])).toEqual('[{"a":{"a":"..."}},{"a":{"a":"..."}}]'); }); });