Files
react/React/ImmutableObject.js
T
2013-06-28 14:30:47 -07:00

169 lines
5.1 KiB
JavaScript

/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule ImmutableObject
*/
"use strict";
var keyMirror = require("./keyMirror");
var merge = require("./merge");
var mergeInto = require("./mergeInto");
var mergeHelpers = require("./mergeHelpers");
var throwIf = require("./throwIf");
var checkMergeObjectArgs = mergeHelpers.checkMergeObjectArgs;
var isTerminal = mergeHelpers.isTerminal;
/**
* Simple wrapper around javascript key/value objects that provide a dev time
* guarantee of immutability (assuming all modules that may potentially mutate
* include a "use strict" declaration). Retaining immutability requires CPU
* cycles (in order to perform the freeze), but this computation can be avoided
* in production. The fact that mutation attempts in __DEV__ will be caught,
* allows us to reasonably assume that mutation on those objects won't even be
* attempted in production. This means that an object being an instanceof
* `ImmutableObject` implies that the object may never change.
*
* TODO: Require strict mode in source files that use ImmutableObject (lint
* rule).
*
* @class ImmutableObject
*/
var ERRORS;
var ImmutableObject;
if (true) {
ERRORS = {
INVALID_MAP_SET_ARG: 'You have attempted to set fields on an object that ' +
'is not an instance of ImmutableObject'
};
/**
* Constructs an instance of `ImmutableObject`.
*
* @param {!Object} initMap The initial set of properties.
* @constructor
*/
ImmutableObject = function(initMap) {
mergeInto(this, initMap);
deepFreeze(this, initMap);
};
/**
* Objects that are instances of `ImmutableObject` are assumed to be deep
* frozen.
* @param {!Object} o The object to deep freeze.
* @return {!boolean} Whether or not deep freeze is needed.
*/
var shouldRecurseFreeze = function(o) {
return (typeof o) === 'object' &&
!(o instanceof ImmutableObject) && o !== null;
};
/**
* Freezes an object `o` deeply. Invokes `shouldRecurseFreeze` to determine if
* further freezing is needed.
*
* @param {!Object} o The object to freeze.
*/
var deepFreeze = function(o) {
var prop;
Object.freeze(o); // First freeze the object.
for (prop in o) {
var field = o[prop];
if (o.hasOwnProperty(prop) && shouldRecurseFreeze(field)) {
deepFreeze(field);
}
}
};
/**
* Returns a new ImmutableObject that is that is the same as the parameter
* `immutableObject` but with the differences specified in `put`.
* @param {!ImmutableObject} ImmutableObject The ImmutableObject object to set
* fields on.
* @param {!Object} put Subset of fields to merge into the returned result.
* @return {!ImmutableObject} The result of merging in `put` fields.
*/
ImmutableObject.set = function(immutableObject, put) {
throwIf(
!(immutableObject instanceof ImmutableObject),
ERRORS.INVALID_MAP_SET_ARG
);
var totalNewFields = merge(immutableObject, put);
return new ImmutableObject(totalNewFields);
};
}
/**
* Sugar for `ImmutableObject.set(ImmutableObject, {fieldName: putField})`
* @see ImmutableObject.set
*/
ImmutableObject.setField = function(immutableObject, fieldName, putField) {
var put = {};
put[fieldName] = putField;
return ImmutableObject.set(immutableObject, put);
};
/**
* Returns a new ImmutableObject that is that is the same as the parameter
* `immutableObject` but with the differences specified in `put` recursively
* applied.
*/
ImmutableObject.setDeep = function(immutableObject, put) {
throwIf(
!(immutableObject instanceof ImmutableObject),
ERRORS.INVALID_MAP_SET_ARG
);
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]);
}
}
// Apply any new keys that the base object didn't have.
var newKeys = Object.keys(put);
for (ii = 0; ii < newKeys.length; ii++) {
var newKey = newKeys[ii];
if (object.hasOwnProperty(newKey)) {
continue;
}
totalNewFields[newKey] = put[newKey];
}
return (object instanceof ImmutableObject || put instanceof ImmutableObject) ?
new ImmutableObject(totalNewFields) :
totalNewFields;
}
module.exports = ImmutableObject;