seamless-immutable.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. (function(){
  2. "use strict";
  3. function addPropertyTo(target, methodName, value) {
  4. Object.defineProperty(target, methodName, {
  5. enumerable: false,
  6. configurable: false,
  7. writable: false,
  8. value: value
  9. });
  10. }
  11. function banProperty(target, methodName) {
  12. addPropertyTo(target, methodName, function() {
  13. throw new ImmutableError("The " + methodName +
  14. " method cannot be invoked on an Immutable data structure.");
  15. });
  16. }
  17. var immutabilityTag = "__immutable_invariants_hold";
  18. function addImmutabilityTag(target) {
  19. addPropertyTo(target, immutabilityTag, true);
  20. }
  21. function isImmutable(target) {
  22. if (typeof target === "object") {
  23. return target === null || target.hasOwnProperty(immutabilityTag);
  24. } else {
  25. // In JavaScript, only objects are even potentially mutable.
  26. // strings, numbers, null, and undefined are all naturally immutable.
  27. return true;
  28. }
  29. }
  30. function isMergableObject(target) {
  31. return target !== null && typeof target === "object" && !(target instanceof Array) && !(target instanceof Date);
  32. }
  33. var mutatingObjectMethods = [
  34. "setPrototypeOf"
  35. ];
  36. var nonMutatingObjectMethods = [
  37. "keys"
  38. ];
  39. var mutatingArrayMethods = mutatingObjectMethods.concat([
  40. "push", "pop", "sort", "splice", "shift", "unshift", "reverse"
  41. ]);
  42. var nonMutatingArrayMethods = nonMutatingObjectMethods.concat([
  43. "map", "filter", "slice", "concat", "reduce", "reduceRight"
  44. ]);
  45. function ImmutableError(message) {
  46. var err = new Error(message);
  47. err.__proto__ = ImmutableError;
  48. return err;
  49. }
  50. ImmutableError.prototype = Error.prototype;
  51. function makeImmutable(obj, bannedMethods) {
  52. // Tag it so we can quickly tell it's immutable later.
  53. addImmutabilityTag(obj);
  54. if ("development" === "development") {
  55. // Make all mutating methods throw exceptions.
  56. for (var index in bannedMethods) {
  57. if (bannedMethods.hasOwnProperty(index)) {
  58. banProperty(obj, bannedMethods[index]);
  59. }
  60. }
  61. // Freeze it and return it.
  62. Object.freeze(obj);
  63. }
  64. return obj;
  65. }
  66. function makeMethodReturnImmutable(obj, methodName) {
  67. var currentMethod = obj[methodName];
  68. addPropertyTo(obj, methodName, function() {
  69. return Immutable(currentMethod.apply(obj, arguments));
  70. });
  71. }
  72. function makeImmutableArray(array) {
  73. // Don't change their implementations, but wrap these functions to make sure
  74. // they always return an immutable value.
  75. for (var index in nonMutatingArrayMethods) {
  76. if (nonMutatingArrayMethods.hasOwnProperty(index)) {
  77. var methodName = nonMutatingArrayMethods[index];
  78. makeMethodReturnImmutable(array, methodName);
  79. }
  80. }
  81. addPropertyTo(array, "flatMap", flatMap);
  82. addPropertyTo(array, "asObject", asObject);
  83. addPropertyTo(array, "asMutable", asMutableArray);
  84. for(var i = 0, length = array.length; i < length; i++) {
  85. array[i] = Immutable(array[i]);
  86. }
  87. return makeImmutable(array, mutatingArrayMethods);
  88. }
  89. /**
  90. * Effectively performs a map() over the elements in the array, using the
  91. * provided iterator, except that whenever the iterator returns an array, that
  92. * array's elements are added to the final result instead of the array itself.
  93. *
  94. * @param {function} iterator - The iterator function that will be invoked on each element in the array. It will receive three arguments: the current value, the current index, and the current object.
  95. */
  96. function flatMap(iterator) {
  97. // Calling .flatMap() with no arguments is a no-op. Don't bother cloning.
  98. if (arguments.length === 0) {
  99. return this;
  100. }
  101. var result = [],
  102. length = this.length,
  103. index;
  104. for (index = 0; index < length; index++) {
  105. var iteratorResult = iterator(this[index], index, this);
  106. if (iteratorResult instanceof Array) {
  107. // Concatenate Array results into the return value we're building up.
  108. result.push.apply(result, iteratorResult);
  109. } else {
  110. // Handle non-Array results the same way map() does.
  111. result.push(iteratorResult);
  112. }
  113. }
  114. return makeImmutableArray(result);
  115. }
  116. /**
  117. * Returns an Immutable copy of the object without the given keys included.
  118. *
  119. * @param {array} keysToRemove - A list of strings representing the keys to exclude in the return value. Instead of providing a single array, this method can also be called by passing multiple strings as separate arguments.
  120. */
  121. function without(keysToRemove) {
  122. // Calling .without() with no arguments is a no-op. Don't bother cloning.
  123. if (arguments.length === 0) {
  124. return this;
  125. }
  126. // If we weren't given an array, use the arguments list.
  127. if (!(keysToRemove instanceof Array)) {
  128. keysToRemove = Array.prototype.slice.call(arguments);
  129. }
  130. var result = this.instantiateEmptyObject();
  131. for (var key in this) {
  132. if (this.hasOwnProperty(key) && (keysToRemove.indexOf(key) === -1)) {
  133. result[key] = this[key];
  134. }
  135. }
  136. return makeImmutableObject(result,
  137. {instantiateEmptyObject: this.instantiateEmptyObject});
  138. }
  139. function asMutableArray(opts) {
  140. var result = [], i, length;
  141. if(opts && opts.deep) {
  142. for(i = 0, length = this.length; i < length; i++) {
  143. result.push( asDeepMutable(this[i]) );
  144. }
  145. } else {
  146. for(i = 0, length = this.length; i < length; i++) {
  147. result.push(this[i]);
  148. }
  149. }
  150. return result;
  151. }
  152. /**
  153. * Effectively performs a [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) over the elements in the array, expecting that the iterator function
  154. * will return an array of two elements - the first representing a key, the other
  155. * a value. Then returns an Immutable Object constructed of those keys and values.
  156. *
  157. * @param {function} iterator - A function which should return an array of two elements - the first representing the desired key, the other the desired value.
  158. */
  159. function asObject(iterator) {
  160. // If no iterator was provided, assume the identity function
  161. // (suggesting this array is already a list of key/value pairs.)
  162. if (typeof iterator !== "function") {
  163. iterator = function(value) { return value; };
  164. }
  165. var result = {},
  166. length = this.length,
  167. index;
  168. for (index = 0; index < length; index++) {
  169. var pair = iterator(this[index], index, this),
  170. key = pair[0],
  171. value = pair[1];
  172. result[key] = value;
  173. }
  174. return makeImmutableObject(result);
  175. }
  176. function asDeepMutable(obj) {
  177. if(!obj || !obj.hasOwnProperty(immutabilityTag) || obj instanceof Date) { return obj; }
  178. return obj.asMutable({deep: true});
  179. }
  180. function quickCopy(src, dest) {
  181. for (var key in src) {
  182. if (src.hasOwnProperty(key)) {
  183. dest[key] = src[key];
  184. }
  185. }
  186. return dest;
  187. }
  188. /**
  189. * Returns an Immutable Object containing the properties and values of both
  190. * this object and the provided object, prioritizing the provided object's
  191. * values whenever the same key is present in both objects.
  192. *
  193. * @param {object} other - The other object to merge. Multiple objects can be passed as an array. In such a case, the later an object appears in that list, the higher its priority.
  194. * @param {object} config - Optional config object that contains settings. Supported settings are: {deep: true} for deep merge and {merger: mergerFunc} where mergerFunc is a function
  195. * that takes a property from both objects. If anything is returned it overrides the normal merge behaviour.
  196. */
  197. function merge(other, config) {
  198. // Calling .merge() with no arguments is a no-op. Don't bother cloning.
  199. if (arguments.length === 0) {
  200. return this;
  201. }
  202. if (other === null || (typeof other !== "object")) {
  203. throw new TypeError("Immutable#merge can only be invoked with objects or arrays, not " + JSON.stringify(other));
  204. }
  205. var anyChanges = false,
  206. result = quickCopy(this, this.instantiateEmptyObject()), // A shallow clone of this object.
  207. receivedArray = (other instanceof Array),
  208. deep = config && config.deep,
  209. merger = config && config.merger,
  210. key;
  211. // Use the given key to extract a value from the given object, then place
  212. // that value in the result object under the same key. If that resulted
  213. // in a change from this object's value at that key, set anyChanges = true.
  214. function addToResult(currentObj, otherObj, key) {
  215. var immutableValue = Immutable(otherObj[key]);
  216. var mergerResult = merger && merger(currentObj[key], immutableValue, config);
  217. if (merger && mergerResult && mergerResult === currentObj[key]) return;
  218. anyChanges = anyChanges ||
  219. mergerResult !== undefined ||
  220. (!currentObj.hasOwnProperty(key) ||
  221. ((immutableValue !== currentObj[key]) &&
  222. // Avoid false positives due to (NaN !== NaN) evaluating to true
  223. (immutableValue === immutableValue)));
  224. if (mergerResult) {
  225. result[key] = mergerResult;
  226. } else if (deep && isMergableObject(currentObj[key]) && isMergableObject(immutableValue)) {
  227. result[key] = currentObj[key].merge(immutableValue, config);
  228. } else {
  229. result[key] = immutableValue;
  230. }
  231. }
  232. // Achieve prioritization by overriding previous values that get in the way.
  233. if (!receivedArray) {
  234. // The most common use case: just merge one object into the existing one.
  235. for (key in other) {
  236. if (other.hasOwnProperty(key)) {
  237. addToResult(this, other, key);
  238. }
  239. }
  240. } else {
  241. // We also accept an Array
  242. for (var index=0; index < other.length; index++) {
  243. var otherFromArray = other[index];
  244. for (key in otherFromArray) {
  245. if (otherFromArray.hasOwnProperty(key)) {
  246. addToResult(this, otherFromArray, key);
  247. }
  248. }
  249. }
  250. }
  251. if (anyChanges) {
  252. return makeImmutableObject(result,
  253. {instantiateEmptyObject: this.instantiateEmptyObject});
  254. } else {
  255. return this;
  256. }
  257. }
  258. function asMutableObject(opts) {
  259. var result = this.instantiateEmptyObject(), key;
  260. if(opts && opts.deep) {
  261. for (key in this) {
  262. if (this.hasOwnProperty(key)) {
  263. result[key] = asDeepMutable(this[key]);
  264. }
  265. }
  266. } else {
  267. for (key in this) {
  268. if (this.hasOwnProperty(key)) {
  269. result[key] = this[key];
  270. }
  271. }
  272. }
  273. return result;
  274. }
  275. // Creates plain object to be used for cloning
  276. function instantiatePlainObject() {
  277. return {};
  278. }
  279. // Finalizes an object with immutable methods, freezes it, and returns it.
  280. function makeImmutableObject(obj, options) {
  281. var instantiateEmptyObject =
  282. (options && options.instantiateEmptyObject) ?
  283. options.instantiateEmptyObject : instantiatePlainObject;
  284. addPropertyTo(obj, "merge", merge);
  285. addPropertyTo(obj, "without", without);
  286. addPropertyTo(obj, "asMutable", asMutableObject);
  287. addPropertyTo(obj, "instantiateEmptyObject", instantiateEmptyObject);
  288. return makeImmutable(obj, mutatingObjectMethods);
  289. }
  290. function Immutable(obj, options) {
  291. if (isImmutable(obj)) {
  292. return obj;
  293. } else if (obj instanceof Array) {
  294. return makeImmutableArray(obj.slice());
  295. } else if (obj instanceof Date) {
  296. return makeImmutable(new Date(obj.getTime()));
  297. } else {
  298. // Don't freeze the object we were given; make a clone and use that.
  299. var prototype = options && options.prototype;
  300. var instantiateEmptyObject =
  301. (!prototype || prototype === Object.prototype) ?
  302. instantiatePlainObject : (function() { return Object.create(prototype); });
  303. var clone = instantiateEmptyObject();
  304. for (var key in obj) {
  305. if (obj.hasOwnProperty(key)) {
  306. clone[key] = Immutable(obj[key]);
  307. }
  308. }
  309. return makeImmutableObject(clone,
  310. {instantiateEmptyObject: instantiateEmptyObject});
  311. }
  312. }
  313. // Export the library
  314. Immutable.isImmutable = isImmutable;
  315. Immutable.ImmutableError = ImmutableError;
  316. Object.freeze(Immutable);
  317. /* istanbul ignore if */
  318. if (typeof module === "object") {
  319. module.exports = Immutable;
  320. } else if (typeof exports === "object") {
  321. exports.Immutable = Immutable;
  322. } else if (typeof window === "object") {
  323. window.Immutable = Immutable;
  324. } else if (typeof global === "object") {
  325. global.Immutable = Immutable;
  326. }
  327. })();