/** * # ResurrectJS * @version 1.0.0 * * ResurrectJS preserves object behavior (prototypes) and reference * circularity with a special JSON encoding. Unlike regular JSON, * Date, RegExp, DOM objects, and `undefined` are also properly * preserved. * * ## Examples * * function Foo() {} * Foo.prototype.greet = function() { return "hello"; }; * * // Behavior is preserved: * var necromancer = new Resurrect(); * var json = necromancer.stringify(new Foo()); * var foo = necromancer.resurrect(json); * foo.greet(); // => "hello" * * // References to the same object are preserved: * json = necromancer.stringify([foo, foo]); * var array = necromancer.resurrect(json); * array[0] === array[1]; // => true * array[1].greet(); // => "hello" * * // Dates are restored properly * json = necromancer.stringify(new Date()); * var date = necromancer.resurrect(json); * Object.prototype.toString.call(date); // => "[object Date]" * * ## Options * * Options are provided to the constructor as an object with these * properties: * * prefix ('#'): A prefix string used for temporary properties added * to objects during serialization and deserialization. It is * important that you don't use any properties beginning with this * string. This option must be consistent between both * serialization and deserialization. * * cleanup (false): Perform full property cleanup after both * serialization and deserialization using the `delete` * operator. This may cause performance penalties (breaking hidden * classes in V8) on objects that ResurrectJS touches, so enable * with care. * * revive (true): Restore behavior (__proto__) to objects that have * been resurrected. If this is set to false during serialization, * resurrection information will not be encoded. You still get * circularity and Date support. * * resolver (Resurrect.NamespaceResolver(window)): Converts between * a name and a prototype. Create a custom resolver if your * constructors are not stored in global variables. The resolver * has two methods: getName(object) and getPrototype(string). * * For example, * * var necromancer = new Resurrect({ * prefix: '__#', * cleanup: true * }); * * ## Caveats * * * With the default resolver, all constructors must be named and * stored in the global variable under that name. This is required * so that the prototypes can be looked up and reconnected at * resurrection time. * * * The wrapper objects Boolean, String, and Number will be * unwrapped. This means extra properties added to these objects * will not be preserved. * * * Functions cannot ever be serialized. Resurrect will throw an * error if a function is found when traversing a data structure. * * @see http://nullprogram.com/blog/2013/03/28/ */ /** * @namespace * @constructor */ function Resurrect(options) { this.table = null; this.prefix = '#'; this.cleanup = false; this.revive = true; for (var option in options) { if (options.hasOwnProperty(option)) { this[option] = options[option]; } } this.refcode = this.prefix + 'id'; this.origcode = this.prefix + 'original'; this.buildcode = this.prefix + '.'; this.valuecode = this.prefix + 'v'; } /** * Portable access to the global object (window, global). * Uses indirect eval. * @constant */ Resurrect.GLOBAL = (0, eval)('this'); /* Helper Objects */ /** * @constructor */ Resurrect.prototype.Error = function ResurrectError(message) { this.message = message || ''; this.stack = new Error().stack; }; Resurrect.prototype.Error.prototype = Object.create(Error.prototype); Resurrect.prototype.Error.prototype.name = 'ResurrectError'; /** * Resolves prototypes through the properties on an object and * constructor names. * @constructor */ Resurrect.NamespaceResolver = function(scope) { this.scope = scope; }; /** * Gets the prototype of the given property name from an object. If * not found, it throws an error. * @param {string} name * @method */ Resurrect.NamespaceResolver.prototype.getPrototype = function(name) { var constructor = this.scope[name]; if (constructor) { return constructor.prototype; } else { throw new Resurrect.prototype.Error('Unknown constructor: ' + name); } }; /** * Get the prototype name for an object, to be fetched later with * getPrototype. * @returns {string} Null if the constructor is Object. * @method */ Resurrect.NamespaceResolver.prototype.getName = function(object) { var constructor = object.constructor.name; if (constructor == null) { // IE var funcPattern = /^\s*function\s*([A-Za-z0-9_$]*)/; constructor = funcPattern.exec(object.constructor)[1]; } if (constructor === '') { var msg = "Can't serialize objects with anonymous constructors."; throw new Resurrect.prototype.Error(msg); } else if (constructor === 'Object' || constructor === 'Array') { return null; } else { return constructor; } }; /* Set the default resolver searches the global object. */ Resurrect.prototype.resolver = new Resurrect.NamespaceResolver(Resurrect.GLOBAL); /** * Create a DOM node from HTML source; behaves like a constructor. * @constructor */ Resurrect.Node = function(html) { var div = document.createElement('div'); div.innerHTML = html; return div.firstChild; }; /* Type Tests */ /** * @param {string} type * @returns {Function} A function that tests for type. */ Resurrect.is = function(type) { var string = '[object ' + type + ']'; return function(object) { return Object.prototype.toString.call(object) === string; }; }; Resurrect.isArray = Resurrect.is('Array'); Resurrect.isString = Resurrect.is('String'); Resurrect.isBoolean = Resurrect.is('Boolean'); Resurrect.isNumber = Resurrect.is('Number'); Resurrect.isFunction = Resurrect.is('Function'); Resurrect.isDate = Resurrect.is('Date'); Resurrect.isRegExp = Resurrect.is('RegExp'); Resurrect.isObject = Resurrect.is('Object'); Resurrect.isAtom = function(object) { return !Resurrect.isObject(object) && !Resurrect.isArray(object); }; /* Methods */ /** * Create a reference (encoding) to an object. * @method */ Resurrect.prototype.ref = function(object) { var ref = {}; if (object === undefined) { ref[this.prefix] = -1; } else { ref[this.prefix] = object[this.refcode]; } return ref; }; /** * Lookup an object in the table by reference object. * @method */ Resurrect.prototype.deref = function(ref) { return this.table[ref[this.prefix]]; }; /** * Put a temporary identifier on an object and store it in the table. * @returns {number} The unique identifier number. * @method */ Resurrect.prototype.tag = function(object) { if (this.revive) { var constructor = this.resolver.getName(object); if (constructor) { var proto = Object.getPrototypeOf(object); if (this.resolver.getPrototype(constructor) !== proto) { throw new this.Error('Constructor mismatch!'); } else { object[this.prefix] = constructor; } } } object[this.refcode] = this.table.length; this.table.push(object); return object[this.refcode]; }; /** * Create a builder object (encoding) for serialization. * @param {string} name The name of the constructor. * @param value The value to pass to the constructor. * @method */ Resurrect.prototype.builder = function(name, value) { var builder = {}; builder[this.buildcode] = name; builder[this.valuecode] = value; return builder; }; /** * Build a value from a deserialized builder. * @method * @see http://stackoverflow.com/a/14378462 * @see http://nullprogram.com/blog/2013/03/24/ */ Resurrect.prototype.build = function(ref) { var type = ref[this.buildcode].split(/\./).reduce(function(object, name) { return object[name]; }, Resurrect.GLOBAL); /* Brilliant hack by kybernetikos: */ var args = [null].concat(ref[this.valuecode]); var factory = type.bind.apply(type, args); return new factory(); }; /** * Dereference or build an object or value from an encoding. * @method */ Resurrect.prototype.decode = function(ref) { if (this.prefix in ref) { return this.deref(ref); } else if (this.buildcode in ref) { return this.build(ref); } else { throw new this.Error('Unknown encoding.'); } }; /** * @returns True if the provided object is already tagged for serialization. * @method */ Resurrect.prototype.isTagged = function(object) { return (this.refcode in object) && (object[this.refcode] != null); }; /** * Visit root and all its ancestors, visiting atoms with f. * @param {Function} f * @returns A fresh copy of root to be serialized. * @method */ Resurrect.prototype.visit = function(root, f) { if (Resurrect.isAtom(root)) { return f(root); } else if (!this.isTagged(root)) { var copy = null; if (Resurrect.isArray(root)) { copy = []; root[this.refcode] = this.tag(copy); for (var i = 0; i < root.length; i++) { copy.push(this.visit(root[i], f)); } } else { /* Object */ copy = Object.create(Object.getPrototypeOf(root)); root[this.refcode] = this.tag(copy); for (var key in root) { if (root.hasOwnProperty(key)) { copy[key] = this.visit(root[key], f); } } } copy[this.origcode] = root; return this.ref(copy); } else { return this.ref(root); } }; /** * Manage special atom values, possibly returning an encoding. */ Resurrect.prototype.handleAtom = function(atom) { var Node = Resurrect.GLOBAL.Node || function() {}; if (Resurrect.isFunction(atom)) { throw new this.Error("Can't serialize functions."); } else if (atom instanceof Node) { var xmls = new XMLSerializer(); return this.builder('Resurrect.Node', [xmls.serializeToString(atom)]); } else if (Resurrect.isDate(atom)) { return this.builder('Date', [atom.toISOString()]); } else if (Resurrect.isRegExp(atom)) { var args = atom.toString().match(/\/(.+)\/([gimy]*)/).slice(1); return this.builder('RegExp', args); } else if (atom === undefined) { return this.ref(undefined); } else { return atom; } }; /** * Serialize an arbitrary JavaScript object, carefully preserving it. * @param object * @param {(Function|Array)} replacer * @param {string} space * @method */ Resurrect.prototype.stringify = function(object, replacer, space) { if (Resurrect.isAtom(object)) { return JSON.stringify(this.handleAtom(object), replacer, space); } else { this.table = []; this.visit(object, this.handleAtom.bind(this)); for (var i = 0; i < this.table.length; i++) { if (this.cleanup) { delete this.table[i][this.origcode][this.refcode]; } else { this.table[i][this.origcode][this.refcode] = null; } delete this.table[i][this.refcode]; delete this.table[i][this.origcode]; } var table = this.table; this.table = null; return JSON.stringify(table, replacer, space); } }; /** * Restore the __proto__ of the given object to the proper value. * @returns The object. */ Resurrect.prototype.fixPrototype = function(object) { if (this.prefix in object) { var name = object[this.prefix]; var prototype = this.resolver.getPrototype(name); if ('__proto__' in object) { object.__proto__ = prototype; if (this.cleanup) { delete object[this.prefix]; } return object; } else { // IE var copy = Object.create(prototype); for (var key in object) { if (object.hasOwnProperty(key) && key !== this.prefix) { copy[key] = object[key]; } } return copy; } } else { return object; } }; /** * Deserialize an encoded object, restoring circularity and behavior. * @param {string} string * @returns The decoded object or value. * @method */ Resurrect.prototype.resurrect = function(string) { var result = null; var data = JSON.parse(string); if (Resurrect.isArray(data)) { this.table = data; /* Restore __proto__. */ if (this.revive) { for (var i = 0; i < this.table.length; i++) { this.table[i] = this.fixPrototype(this.table[i]); } } /* Re-establish object references and construct atoms. */ for (i = 0; i < this.table.length; i++) { var object = this.table[i]; for (var key in object) { if (object.hasOwnProperty(key)) { if (!(Resurrect.isAtom(object[key]))) { object[key] = this.decode(object[key]); } } } } result = this.table[0]; } else if (Resurrect.isObject(data)) { this.table = []; result = this.decode(data); } else { result = data; } this.table = null; return result; };