diff --git a/src/utils/ImmutableObject.js b/src/utils/ImmutableObject.js index e24ea68e59..957f2c44ac 100644 --- a/src/utils/ImmutableObject.js +++ b/src/utils/ImmutableObject.js @@ -14,178 +14,145 @@ * limitations under the License. * * @providesModule ImmutableObject - * @typechecks */ "use strict"; +var Immutable = require('Immutable'); + var invariant = require('invariant'); -var isNode = require('isNode'); -var merge = require('merge'); -var mergeInto = require('mergeInto'); +var keyOf = require('keyOf'); var mergeHelpers = require('mergeHelpers'); var checkMergeObjectArgs = mergeHelpers.checkMergeObjectArgs; var isTerminal = mergeHelpers.isTerminal; -/** - * Wrapper around JavaScript objects that provide a guarantee of immutability at - * developer time when strict mode is used. The extra computations required to - * enforce immutability is stripped out in production for performance reasons. - */ -var ImmutableObject; +var SECRET_KEY = keyOf({_DONT_EVER_TYPE_THIS_SECRET_KEY: null}); -function assertImmutableObject(immutableObject) { +/** + * Static methods creating and operating on instances of `Immutable`. + */ +function assertImmutable(immutable) { invariant( - immutableObject instanceof ImmutableObject, + immutable instanceof Immutable, 'ImmutableObject: Attempted to set fields on an object that is not an ' + - 'instance of ImmutableObject.' + 'instance of Immutable.' ); } -if (__DEV__) { - /** - * Constructs an instance of `ImmutableObject`. - * - * @param {?object} initialProperties The initial set of properties. - * @constructor - */ - ImmutableObject = function ImmutableObject(initialProperties) { - mergeInto(this, initialProperties); - deepFreeze(this); - }; - - /** - * Checks if an object should be deep frozen. Instances of `ImmutableObject` - * are assumed to have already been deep frozen. - * - * @param {*} object The object to check. - * @return {boolean} Whether or not deep freeze is needed. - */ - var shouldRecurseFreeze = function(object) { - return ( - typeof object === 'object' && - !(object instanceof ImmutableObject) && - object !== null - ); - }; - - /** - * Freezes the supplied object deeply. - * - * @param {*} object The object to freeze. - */ - var deepFreeze = function(object) { - if (isNode(object)) { - return; // Don't try to freeze DOM nodes. - } - Object.freeze(object); // First freeze the object. - for (var prop in object) { - var field = object[prop]; - if (object.hasOwnProperty(prop) && shouldRecurseFreeze(field)) { - deepFreeze(field); - } - } - }; - - /** - * Returns a new ImmutableObject that is identical to the supplied object but - * with the supplied changes, `put`. - * - * @param {ImmutableObject} immutableObject Starting object. - * @param {?object} put Fields to merge into the object. - * @return {ImmutableObject} The result of merging in `put` fields. - */ - ImmutableObject.set = function(immutableObject, put) { - assertImmutableObject(immutableObject); - var totalNewFields = merge(immutableObject, put); - return new ImmutableObject(totalNewFields); - }; - -} else { - /** - * Constructs an instance of `ImmutableObject`. - * - * @param {?object} initialProperties The initial set of properties. - * @constructor - */ - ImmutableObject = function ImmutableObject(initialProperties) { - mergeInto(this, initialProperties); - }; - - /** - * Returns a new ImmutableObject that is identical to the supplied object but - * with the supplied changes, `put`. - * - * @param {ImmutableObject} immutableObject Starting object. - * @param {?object} put Fields to merge into the object. - * @return {ImmutableObject} The result of merging in `put` fields. - */ - ImmutableObject.set = function(immutableObject, put) { - assertImmutableObject(immutableObject); - var newMap = new ImmutableObject(immutableObject); - mergeInto(newMap, put); - return newMap; - }; -} - /** - * Sugar for `ImmutableObject.set(ImmutableObject, {fieldName: putField})`. - * - * @param {ImmutableObject} immutableObject Object on which to set field. - * @param {string} fieldName Name of the field to set. - * @param {*} putField Value of the field to set. - * @return {ImmutableObject} [description] + * Static methods for reasoning about instances of `ImmutableObject`. Execute + * the freeze commands in `__DEV__` mode to alert the programmer that something + * is attempting to mutate. Since freezing is very expensive, we avoid doing it + * at all in production. */ -ImmutableObject.setField = function(immutableObject, fieldName, putField) { - var put = {}; - put[fieldName] = putField; - return ImmutableObject.set(immutableObject, put); -}; - -/** - * Returns a new ImmutableObject that is identical to the supplied object but - * with the supplied changes recursively applied. - * - * @param {ImmutableObject} immutableObject Object on which to set fields. - * @param {object} put Fields to merge into the object. - * @return {ImmutableObject} The result of merging in `put` fields. - */ -ImmutableObject.setDeep = function(immutableObject, put) { - assertImmutableObject(immutableObject); - return _setDeep(immutableObject, put); -}; - -function _setDeep(object, put) { - checkMergeObjectArgs(object, put); - var totalNewFields = {}; - - // To maintain the order of the keys, copy the base object's entries first. - var keys = Object.keys(object); - for (var ii = 0; ii < keys.length; ii++) { - var key = keys[ii]; - if (!put.hasOwnProperty(key)) { - totalNewFields[key] = object[key]; - } else if (isTerminal(object[key]) || isTerminal(put[key])) { - totalNewFields[key] = put[key]; - } else { - totalNewFields[key] = _setDeep(object[key], put[key]); +class ImmutableObject extends Immutable { + /** + * @arguments {array} The arguments is an array of objects that, when + * merged together, will form the immutable objects. + */ + constructor() { + super(Immutable[SECRET_KEY]); + Immutable.mergeAllPropertiesInto(this, arguments); + if (__DEV__) { + Immutable.deepFreezeRootNode(this); } } - // Apply any new keys that the base object didn't have. + /** + * DEPRECATED - prefer to instantiate with new ImmutableObject(). + * + * @arguments {array} The arguments is an array of objects that, when + * merged together, will form the immutable objects. + */ + static create() { + var obj = Object.create(ImmutableObject.prototype); + ImmutableObject.apply(obj, arguments); + return obj; + } + + /** + * Returns a new `Immutable` that is identical to the supplied `Immutable` + * but with the specified changes, `put`. Any keys that are in the + * intersection of `immutable` and `put` retain the ordering of `immutable. + * New keys are placed after keys that exist in `immutable`. + * + * @param {Immutable} immutable Starting object. + * @param {?object} put Fields to merge into the object. + * @return {Immutable} The result of merging in `put` fields. + */ + static set(immutable, put) { + assertImmutable(immutable); + invariant( + typeof put === 'object' && put !== undefined && !Array.isArray(put), + 'Invalid ImmutableMap.set argument `put`' + ); + return new ImmutableObject(immutable, put); + } + + /** + * Sugar for `ImmutableObject.set(ImmutableObject, {fieldName: putField})`. + * Look out for key crushing: Use `keyOf()` to guard against it. + * + * @param {Immutable} immutable Object on which to set properties. + * @param {string} fieldName Name of the field to set. + * @param {*} putField Value of the field to set. + * @return {Immutable} new Immutable as described in `set`. + */ + static setProperty(immutableObject, fieldName, putField) { + var put = {}; + put[fieldName] = putField; + return ImmutableObject.set(immutableObject, put); + } + + /** + * Returns a new `Immutable` that is identical to the supplied object but + * with the supplied changes recursively applied. + * + * Experimental. Likely does not handle `Arrays` correctly. + * + * @param {Immutable} immutable Object on which to set fields. + * @param {object} put Fields to merge into the object. + * @return {Immutable} The result of merging in `put` fields. + */ + static setDeep(immutable, put) { + assertImmutable(immutable); + return _setDeep(immutable, put); + } +} + +function _setDeep(obj, put) { + checkMergeObjectArgs(obj, put); + var totalNewFields = {}; + + // To maintain the order of the keys, copy the base object's entries first. + var keys = Object.keys(obj); + for (var ii = 0; ii < keys.length; ii++) { + var key = keys[ii]; + if (!put.hasOwnProperty(key)) { + totalNewFields[key] = obj[key]; + } else if (isTerminal(obj[key]) || isTerminal(put[key])) { + totalNewFields[key] = put[key]; + } else { + totalNewFields[key] = _setDeep(obj[key], put[key]); + } + } + + // Apply any new keys that the base obj didn't have. var newKeys = Object.keys(put); for (ii = 0; ii < newKeys.length; ii++) { var newKey = newKeys[ii]; - if (object.hasOwnProperty(newKey)) { + if (obj.hasOwnProperty(newKey)) { continue; } totalNewFields[newKey] = put[newKey]; } - return (object instanceof ImmutableObject || put instanceof ImmutableObject) ? - new ImmutableObject(totalNewFields) : - totalNewFields; + return ( + obj instanceof Immutable ? new ImmutableObject(totalNewFields) : + put instanceof Immutable ? new ImmutableObject(totalNewFields) : + totalNewFields + ); } module.exports = ImmutableObject; diff --git a/src/utils/__tests__/ImmutableObject-test.js b/src/utils/__tests__/ImmutableObject-test.js index 7dfb7374ce..d251d30d77 100644 --- a/src/utils/__tests__/ImmutableObject-test.js +++ b/src/utils/__tests__/ImmutableObject-test.js @@ -18,10 +18,8 @@ "use strict"; -require('mock-modules') - .dontMock('ImmutableObject'); - var ImmutableObject; +var Immutable; /** * To perform performance testing of using `ImmutableObject` vs. not using @@ -30,40 +28,27 @@ var ImmutableObject; describe('ImmutableObject', function() { var message; beforeEach(function() { - require('mock-modules').dumpCache(); ImmutableObject = require('ImmutableObject'); - this.addMatchers({ - /** - * Equivalent with respect to serialization. Must stringify because - * constructors are different and other comparison methods will not - * consider them structurally equal. Probably not useful for use outside - * of this test module. - */ - toBeSeriallyEqualTo: function(expected) { - var actual = this.actual; - var notText = this.isNot ? " not" : ""; - this.message = function () { - return "Expected " + JSON.stringify(actual) + notText + - " to be serially equal to " + JSON.stringify(expected); - }; - - return JSON.stringify(actual) === JSON.stringify(expected); - } - }); + Immutable = require('Immutable'); }); - /** - * We are in __DEV__ by default. - */ var testDev = function(message, testFunc) { - it(message, testFunc); + it(message, function() { + var old = window.__DEV__; + window.__DEV__ = true; + testFunc(); + window.__DEV__ = old; + }); }; var testProd = function(message, testFunc) { - // Temporarily enter production mode - window.__DEV__ = false; - it(message, testFunc); - window.__DEV__ = true; + it(message, function() { + // Temporarily enter production mode + var old = window.__DEV__; + window.__DEV__ = false; + testFunc(); + window.__DEV__ = old; + }); }; var testDevAndProd = function(message, testFunc) { @@ -89,6 +74,12 @@ describe('ImmutableObject', function() { }).not.toThrow(); }); + testDevAndProd('should extend Immutable', function() { + var object = new ImmutableObject({foo: 'bar'}); + expect (object instanceof Immutable).toBe(true); + expect (object instanceof ImmutableObject).toBe(true); + }); + testDev('should not exceed maximum call stack size with nodes', function() { var node = document.createElement('div'); var object = new ImmutableObject({node: node}); @@ -142,7 +133,7 @@ describe('ImmutableObject', function() { var beforeIO = new ImmutableObject({shallowField: {deepField: {oldField: null}}}); var afterIO = ImmutableObject.set(beforeIO, {}); - expect(afterIO).toBeSeriallyEqualTo(beforeIO); + expect(afterIO).toSeriallyEqual(beforeIO); expect(afterIO).not.toBe(beforeIO); } ); @@ -173,7 +164,7 @@ describe('ImmutableObject', function() { var beforeIO = new ImmutableObject(beforeStructure); var afterIO = ImmutableObject.set(beforeIO, delta); - expect(afterIO).toBeSeriallyEqualTo(expectedAfterStructure); + expect(afterIO).toSeriallyEqual(expectedAfterStructure); expect(afterIO).not.toBe(beforeIO); } ); @@ -199,7 +190,7 @@ describe('ImmutableObject', function() { var beforeIO = new ImmutableObject(beforeStructure); var afterIO = ImmutableObject.set(beforeIO, delta); - expect(afterIO).toBeSeriallyEqualTo(expectedAfterStructure); + expect(afterIO).toSeriallyEqual(expectedAfterStructure); expect(afterIO).not.toBe(beforeIO); } ); @@ -226,13 +217,13 @@ describe('ImmutableObject', function() { var beforeIO = new ImmutableObject(beforeStructure); var afterIO = ImmutableObject.set(beforeIO, delta); - expect(afterIO).toBeSeriallyEqualTo(expectedAfterStructure); + expect(afterIO).toSeriallyEqual(expectedAfterStructure); expect(afterIO).not.toBe(beforeIO); }); message = 'should tolerate arrays at deeper levels and prevent mutation on them'; - testDevAndProd(message, function() { + testDev(message, function() { if (window.callPhantom) { // PhantomJS has a bug with Object.freeze and Arrays. // https://github.com/ariya/phantomjs/issues/10817 @@ -251,12 +242,12 @@ describe('ImmutableObject', function() { expect(io.shallowField[1]).toEqual('second field'); }); - message = 'should provide a setField interface as sugar for set()'; + message = 'should provide a setProperty interface as sugar for set()'; testDevAndProd(message, function() { var beforeIO = new ImmutableObject({initialField: null}); var afterIO = - ImmutableObject.setField(beforeIO, 'anotherField', 'anotherValue'); - expect(afterIO).toBeSeriallyEqualTo({ + ImmutableObject.setProperty(beforeIO, 'anotherField', 'anotherValue'); + expect(afterIO).toSeriallyEqual({ initialField: null, anotherField: 'anotherValue' }); @@ -271,7 +262,7 @@ describe('ImmutableObject', function() { var afterIO = ImmutableObject.setDeep(beforeIO, { a: {b: {}, c: 'C', e: {f: 'F', g: 'G'}, h: 'H'} }); - expect(afterIO).toBeSeriallyEqualTo({ + expect(afterIO).toSeriallyEqual({ a: {b: {}, c: 'C', d: 'd', e: {f: 'F', g: 'G'}, h: 'H'} }); expect(afterIO).not.toBe(beforeIO); @@ -286,11 +277,12 @@ describe('ImmutableObject', function() { var afterIO = ImmutableObject.setDeep(beforeIO, { a: {b: {d: 'D'}, e: new ImmutableObject({g: 'G'})} }); - expect(afterIO).toBeSeriallyEqualTo({ + expect(afterIO).toSeriallyEqual({ a: {b: {c: 'c', d: 'D'}, e: {f: 'f', g: 'G'}} }); - expect(afterIO instanceof ImmutableObject).toBe(true); - expect(afterIO.a.b instanceof ImmutableObject).toBe(true); - expect(afterIO.a.e instanceof ImmutableObject).toBe(true); + expect(afterIO instanceof Immutable).toBe(true); + expect(afterIO.a.b instanceof Immutable).toBe(true); + expect(afterIO.a.e instanceof Immutable).toBe(true); }); }); + diff --git a/src/utils/immutable/Immutable.js b/src/utils/immutable/Immutable.js index 444d426e47..7c7559a6b5 100644 --- a/src/utils/immutable/Immutable.js +++ b/src/utils/immutable/Immutable.js @@ -1,5 +1,5 @@ /** - * Copyright 2014 Facebook, Inc. + * Copyright 2013-2014 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ class Immutable { /** * Helper method for classes that make use of `Immutable`. * @param {Immutable} immutable Object to merge properties into. - * @param {Array} propertyObjects List of objects to merge into + * @param {array} propertyObjects List of objects to merge into * `destination`. */ static mergeAllPropertiesInto(destination, propertyObjects) {