object.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. import { isBigNumber } from './is.js';
  2. /**
  3. * Clone an object
  4. *
  5. * clone(x)
  6. *
  7. * Can clone any primitive type, array, and object.
  8. * If x has a function clone, this function will be invoked to clone the object.
  9. *
  10. * @param {*} x
  11. * @return {*} clone
  12. */
  13. export function clone(x) {
  14. var type = typeof x;
  15. // immutable primitive types
  16. if (type === 'number' || type === 'string' || type === 'boolean' || x === null || x === undefined) {
  17. return x;
  18. }
  19. // use clone function of the object when available
  20. if (typeof x.clone === 'function') {
  21. return x.clone();
  22. }
  23. // array
  24. if (Array.isArray(x)) {
  25. return x.map(function (value) {
  26. return clone(value);
  27. });
  28. }
  29. if (x instanceof Date) return new Date(x.valueOf());
  30. if (isBigNumber(x)) return x; // bignumbers are immutable
  31. if (x instanceof RegExp) throw new TypeError('Cannot clone ' + x); // TODO: clone a RegExp
  32. // object
  33. return mapObject(x, clone);
  34. }
  35. /**
  36. * Apply map to all properties of an object
  37. * @param {Object} object
  38. * @param {function} callback
  39. * @return {Object} Returns a copy of the object with mapped properties
  40. */
  41. export function mapObject(object, callback) {
  42. var clone = {};
  43. for (var key in object) {
  44. if (hasOwnProperty(object, key)) {
  45. clone[key] = callback(object[key]);
  46. }
  47. }
  48. return clone;
  49. }
  50. /**
  51. * Extend object a with the properties of object b
  52. * @param {Object} a
  53. * @param {Object} b
  54. * @return {Object} a
  55. */
  56. export function extend(a, b) {
  57. for (var prop in b) {
  58. if (hasOwnProperty(b, prop)) {
  59. a[prop] = b[prop];
  60. }
  61. }
  62. return a;
  63. }
  64. /**
  65. * Deep extend an object a with the properties of object b
  66. * @param {Object} a
  67. * @param {Object} b
  68. * @returns {Object}
  69. */
  70. export function deepExtend(a, b) {
  71. // TODO: add support for Arrays to deepExtend
  72. if (Array.isArray(b)) {
  73. throw new TypeError('Arrays are not supported by deepExtend');
  74. }
  75. for (var prop in b) {
  76. // We check against prop not being in Object.prototype or Function.prototype
  77. // to prevent polluting for example Object.__proto__.
  78. if (hasOwnProperty(b, prop) && !(prop in Object.prototype) && !(prop in Function.prototype)) {
  79. if (b[prop] && b[prop].constructor === Object) {
  80. if (a[prop] === undefined) {
  81. a[prop] = {};
  82. }
  83. if (a[prop] && a[prop].constructor === Object) {
  84. deepExtend(a[prop], b[prop]);
  85. } else {
  86. a[prop] = b[prop];
  87. }
  88. } else if (Array.isArray(b[prop])) {
  89. throw new TypeError('Arrays are not supported by deepExtend');
  90. } else {
  91. a[prop] = b[prop];
  92. }
  93. }
  94. }
  95. return a;
  96. }
  97. /**
  98. * Deep test equality of all fields in two pairs of arrays or objects.
  99. * Compares values and functions strictly (ie. 2 is not the same as '2').
  100. * @param {Array | Object} a
  101. * @param {Array | Object} b
  102. * @returns {boolean}
  103. */
  104. export function deepStrictEqual(a, b) {
  105. var prop, i, len;
  106. if (Array.isArray(a)) {
  107. if (!Array.isArray(b)) {
  108. return false;
  109. }
  110. if (a.length !== b.length) {
  111. return false;
  112. }
  113. for (i = 0, len = a.length; i < len; i++) {
  114. if (!deepStrictEqual(a[i], b[i])) {
  115. return false;
  116. }
  117. }
  118. return true;
  119. } else if (typeof a === 'function') {
  120. return a === b;
  121. } else if (a instanceof Object) {
  122. if (Array.isArray(b) || !(b instanceof Object)) {
  123. return false;
  124. }
  125. for (prop in a) {
  126. // noinspection JSUnfilteredForInLoop
  127. if (!(prop in b) || !deepStrictEqual(a[prop], b[prop])) {
  128. return false;
  129. }
  130. }
  131. for (prop in b) {
  132. // noinspection JSUnfilteredForInLoop
  133. if (!(prop in a)) {
  134. return false;
  135. }
  136. }
  137. return true;
  138. } else {
  139. return a === b;
  140. }
  141. }
  142. /**
  143. * Recursively flatten a nested object.
  144. * @param {Object} nestedObject
  145. * @return {Object} Returns the flattened object
  146. */
  147. export function deepFlatten(nestedObject) {
  148. var flattenedObject = {};
  149. _deepFlatten(nestedObject, flattenedObject);
  150. return flattenedObject;
  151. }
  152. // helper function used by deepFlatten
  153. function _deepFlatten(nestedObject, flattenedObject) {
  154. for (var prop in nestedObject) {
  155. if (hasOwnProperty(nestedObject, prop)) {
  156. var value = nestedObject[prop];
  157. if (typeof value === 'object' && value !== null) {
  158. _deepFlatten(value, flattenedObject);
  159. } else {
  160. flattenedObject[prop] = value;
  161. }
  162. }
  163. }
  164. }
  165. /**
  166. * Test whether the current JavaScript engine supports Object.defineProperty
  167. * @returns {boolean} returns true if supported
  168. */
  169. export function canDefineProperty() {
  170. // test needed for broken IE8 implementation
  171. try {
  172. if (Object.defineProperty) {
  173. Object.defineProperty({}, 'x', {
  174. get: function get() {
  175. return null;
  176. }
  177. });
  178. return true;
  179. }
  180. } catch (e) {}
  181. return false;
  182. }
  183. /**
  184. * Attach a lazy loading property to a constant.
  185. * The given function `fn` is called once when the property is first requested.
  186. *
  187. * @param {Object} object Object where to add the property
  188. * @param {string} prop Property name
  189. * @param {Function} valueResolver Function returning the property value. Called
  190. * without arguments.
  191. */
  192. export function lazy(object, prop, valueResolver) {
  193. var _uninitialized = true;
  194. var _value;
  195. Object.defineProperty(object, prop, {
  196. get: function get() {
  197. if (_uninitialized) {
  198. _value = valueResolver();
  199. _uninitialized = false;
  200. }
  201. return _value;
  202. },
  203. set: function set(value) {
  204. _value = value;
  205. _uninitialized = false;
  206. },
  207. configurable: true,
  208. enumerable: true
  209. });
  210. }
  211. /**
  212. * Traverse a path into an object.
  213. * When a namespace is missing, it will be created
  214. * @param {Object} object
  215. * @param {string | string[]} path A dot separated string like 'name.space'
  216. * @return {Object} Returns the object at the end of the path
  217. */
  218. export function traverse(object, path) {
  219. if (path && typeof path === 'string') {
  220. return traverse(object, path.split('.'));
  221. }
  222. var obj = object;
  223. if (path) {
  224. for (var i = 0; i < path.length; i++) {
  225. var key = path[i];
  226. if (!(key in obj)) {
  227. obj[key] = {};
  228. }
  229. obj = obj[key];
  230. }
  231. }
  232. return obj;
  233. }
  234. /**
  235. * A safe hasOwnProperty
  236. * @param {Object} object
  237. * @param {string} property
  238. */
  239. export function hasOwnProperty(object, property) {
  240. return object && Object.hasOwnProperty.call(object, property);
  241. }
  242. /**
  243. * Test whether an object is a factory. a factory has fields:
  244. *
  245. * - factory: function (type: Object, config: Object, load: function, typed: function [, math: Object]) (required)
  246. * - name: string (optional)
  247. * - path: string A dot separated path (optional)
  248. * - math: boolean If true (false by default), the math namespace is passed
  249. * as fifth argument of the factory function
  250. *
  251. * @param {*} object
  252. * @returns {boolean}
  253. */
  254. export function isLegacyFactory(object) {
  255. return object && typeof object.factory === 'function';
  256. }
  257. /**
  258. * Get a nested property from an object
  259. * @param {Object} object
  260. * @param {string | string[]} path
  261. * @returns {Object}
  262. */
  263. export function get(object, path) {
  264. if (typeof path === 'string') {
  265. if (isPath(path)) {
  266. return get(object, path.split('.'));
  267. } else {
  268. return object[path];
  269. }
  270. }
  271. var child = object;
  272. for (var i = 0; i < path.length; i++) {
  273. var key = path[i];
  274. child = child ? child[key] : undefined;
  275. }
  276. return child;
  277. }
  278. /**
  279. * Set a nested property in an object
  280. * Mutates the object itself
  281. * If the path doesn't exist, it will be created
  282. * @param {Object} object
  283. * @param {string | string[]} path
  284. * @param {*} value
  285. * @returns {Object}
  286. */
  287. export function set(object, path, value) {
  288. if (typeof path === 'string') {
  289. if (isPath(path)) {
  290. return set(object, path.split('.'), value);
  291. } else {
  292. object[path] = value;
  293. return object;
  294. }
  295. }
  296. var child = object;
  297. for (var i = 0; i < path.length - 1; i++) {
  298. var key = path[i];
  299. if (child[key] === undefined) {
  300. child[key] = {};
  301. }
  302. child = child[key];
  303. }
  304. if (path.length > 0) {
  305. var lastKey = path[path.length - 1];
  306. child[lastKey] = value;
  307. }
  308. return object;
  309. }
  310. /**
  311. * Create an object composed of the picked object properties
  312. * @param {Object} object
  313. * @param {string[]} properties
  314. * @param {function} [transform] Optional value to transform a value when picking it
  315. * @return {Object}
  316. */
  317. export function pick(object, properties, transform) {
  318. var copy = {};
  319. for (var i = 0; i < properties.length; i++) {
  320. var key = properties[i];
  321. var value = get(object, key);
  322. if (value !== undefined) {
  323. set(copy, key, transform ? transform(value, key) : value);
  324. }
  325. }
  326. return copy;
  327. }
  328. /**
  329. * Shallow version of pick, creating an object composed of the picked object properties
  330. * but not for nested properties
  331. * @param {Object} object
  332. * @param {string[]} properties
  333. * @return {Object}
  334. */
  335. export function pickShallow(object, properties) {
  336. var copy = {};
  337. for (var i = 0; i < properties.length; i++) {
  338. var key = properties[i];
  339. var value = object[key];
  340. if (value !== undefined) {
  341. copy[key] = value;
  342. }
  343. }
  344. return copy;
  345. }
  346. export function values(object) {
  347. return Object.keys(object).map(key => object[key]);
  348. }
  349. // helper function to test whether a string contains a path like 'user.name'
  350. function isPath(str) {
  351. return str.indexOf('.') !== -1;
  352. }