lib/goog/object/object.js

1// Copyright 2006 The Closure Library Authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS-IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/**
16 * @fileoverview Utilities for manipulating objects/maps/hashes.
17 */
18
19goog.provide('goog.object');
20
21goog.require('goog.array');
22
23
24/**
25 * Calls a function for each element in an object/map/hash.
26 *
27 * @param {Object.<K,V>} obj The object over which to iterate.
28 * @param {function(this:T,V,?,Object.<K,V>):?} f The function to call
29 * for every element. This function takes 3 arguments (the element, the
30 * index and the object) and the return value is ignored.
31 * @param {T=} opt_obj This is used as the 'this' object within f.
32 * @template T,K,V
33 */
34goog.object.forEach = function(obj, f, opt_obj) {
35 for (var key in obj) {
36 f.call(opt_obj, obj[key], key, obj);
37 }
38};
39
40
41/**
42 * Calls a function for each element in an object/map/hash. If that call returns
43 * true, adds the element to a new object.
44 *
45 * @param {Object.<K,V>} obj The object over which to iterate.
46 * @param {function(this:T,V,?,Object.<K,V>):boolean} f The function to call
47 * for every element. This
48 * function takes 3 arguments (the element, the index and the object)
49 * and should return a boolean. If the return value is true the
50 * element is added to the result object. If it is false the
51 * element is not included.
52 * @param {T=} opt_obj This is used as the 'this' object within f.
53 * @return {!Object.<K,V>} a new object in which only elements that passed the
54 * test are present.
55 * @template T,K,V
56 */
57goog.object.filter = function(obj, f, opt_obj) {
58 var res = {};
59 for (var key in obj) {
60 if (f.call(opt_obj, obj[key], key, obj)) {
61 res[key] = obj[key];
62 }
63 }
64 return res;
65};
66
67
68/**
69 * For every element in an object/map/hash calls a function and inserts the
70 * result into a new object.
71 *
72 * @param {Object.<K,V>} obj The object over which to iterate.
73 * @param {function(this:T,V,?,Object.<K,V>):R} f The function to call
74 * for every element. This function
75 * takes 3 arguments (the element, the index and the object)
76 * and should return something. The result will be inserted
77 * into a new object.
78 * @param {T=} opt_obj This is used as the 'this' object within f.
79 * @return {!Object.<K,R>} a new object with the results from f.
80 * @template T,K,V,R
81 */
82goog.object.map = function(obj, f, opt_obj) {
83 var res = {};
84 for (var key in obj) {
85 res[key] = f.call(opt_obj, obj[key], key, obj);
86 }
87 return res;
88};
89
90
91/**
92 * Calls a function for each element in an object/map/hash. If any
93 * call returns true, returns true (without checking the rest). If
94 * all calls return false, returns false.
95 *
96 * @param {Object.<K,V>} obj The object to check.
97 * @param {function(this:T,V,?,Object.<K,V>):boolean} f The function to
98 * call for every element. This function
99 * takes 3 arguments (the element, the index and the object) and should
100 * return a boolean.
101 * @param {T=} opt_obj This is used as the 'this' object within f.
102 * @return {boolean} true if any element passes the test.
103 * @template T,K,V
104 */
105goog.object.some = function(obj, f, opt_obj) {
106 for (var key in obj) {
107 if (f.call(opt_obj, obj[key], key, obj)) {
108 return true;
109 }
110 }
111 return false;
112};
113
114
115/**
116 * Calls a function for each element in an object/map/hash. If
117 * all calls return true, returns true. If any call returns false, returns
118 * false at this point and does not continue to check the remaining elements.
119 *
120 * @param {Object.<K,V>} obj The object to check.
121 * @param {?function(this:T,V,?,Object.<K,V>):boolean} f The function to
122 * call for every element. This function
123 * takes 3 arguments (the element, the index and the object) and should
124 * return a boolean.
125 * @param {T=} opt_obj This is used as the 'this' object within f.
126 * @return {boolean} false if any element fails the test.
127 * @template T,K,V
128 */
129goog.object.every = function(obj, f, opt_obj) {
130 for (var key in obj) {
131 if (!f.call(opt_obj, obj[key], key, obj)) {
132 return false;
133 }
134 }
135 return true;
136};
137
138
139/**
140 * Returns the number of key-value pairs in the object map.
141 *
142 * @param {Object} obj The object for which to get the number of key-value
143 * pairs.
144 * @return {number} The number of key-value pairs in the object map.
145 */
146goog.object.getCount = function(obj) {
147 // JS1.5 has __count__ but it has been deprecated so it raises a warning...
148 // in other words do not use. Also __count__ only includes the fields on the
149 // actual object and not in the prototype chain.
150 var rv = 0;
151 for (var key in obj) {
152 rv++;
153 }
154 return rv;
155};
156
157
158/**
159 * Returns one key from the object map, if any exists.
160 * For map literals the returned key will be the first one in most of the
161 * browsers (a know exception is Konqueror).
162 *
163 * @param {Object} obj The object to pick a key from.
164 * @return {string|undefined} The key or undefined if the object is empty.
165 */
166goog.object.getAnyKey = function(obj) {
167 for (var key in obj) {
168 return key;
169 }
170};
171
172
173/**
174 * Returns one value from the object map, if any exists.
175 * For map literals the returned value will be the first one in most of the
176 * browsers (a know exception is Konqueror).
177 *
178 * @param {Object.<K,V>} obj The object to pick a value from.
179 * @return {V|undefined} The value or undefined if the object is empty.
180 * @template K,V
181 */
182goog.object.getAnyValue = function(obj) {
183 for (var key in obj) {
184 return obj[key];
185 }
186};
187
188
189/**
190 * Whether the object/hash/map contains the given object as a value.
191 * An alias for goog.object.containsValue(obj, val).
192 *
193 * @param {Object.<K,V>} obj The object in which to look for val.
194 * @param {V} val The object for which to check.
195 * @return {boolean} true if val is present.
196 * @template K,V
197 */
198goog.object.contains = function(obj, val) {
199 return goog.object.containsValue(obj, val);
200};
201
202
203/**
204 * Returns the values of the object/map/hash.
205 *
206 * @param {Object.<K,V>} obj The object from which to get the values.
207 * @return {!Array.<V>} The values in the object/map/hash.
208 * @template K,V
209 */
210goog.object.getValues = function(obj) {
211 var res = [];
212 var i = 0;
213 for (var key in obj) {
214 res[i++] = obj[key];
215 }
216 return res;
217};
218
219
220/**
221 * Returns the keys of the object/map/hash.
222 *
223 * @param {Object} obj The object from which to get the keys.
224 * @return {!Array.<string>} Array of property keys.
225 */
226goog.object.getKeys = function(obj) {
227 var res = [];
228 var i = 0;
229 for (var key in obj) {
230 res[i++] = key;
231 }
232 return res;
233};
234
235
236/**
237 * Get a value from an object multiple levels deep. This is useful for
238 * pulling values from deeply nested objects, such as JSON responses.
239 * Example usage: getValueByKeys(jsonObj, 'foo', 'entries', 3)
240 *
241 * @param {!Object} obj An object to get the value from. Can be array-like.
242 * @param {...(string|number|!Array.<number|string>)} var_args A number of keys
243 * (as strings, or numbers, for array-like objects). Can also be
244 * specified as a single array of keys.
245 * @return {*} The resulting value. If, at any point, the value for a key
246 * is undefined, returns undefined.
247 */
248goog.object.getValueByKeys = function(obj, var_args) {
249 var isArrayLike = goog.isArrayLike(var_args);
250 var keys = isArrayLike ? var_args : arguments;
251
252 // Start with the 2nd parameter for the variable parameters syntax.
253 for (var i = isArrayLike ? 0 : 1; i < keys.length; i++) {
254 obj = obj[keys[i]];
255 if (!goog.isDef(obj)) {
256 break;
257 }
258 }
259
260 return obj;
261};
262
263
264/**
265 * Whether the object/map/hash contains the given key.
266 *
267 * @param {Object} obj The object in which to look for key.
268 * @param {*} key The key for which to check.
269 * @return {boolean} true If the map contains the key.
270 */
271goog.object.containsKey = function(obj, key) {
272 return key in obj;
273};
274
275
276/**
277 * Whether the object/map/hash contains the given value. This is O(n).
278 *
279 * @param {Object.<K,V>} obj The object in which to look for val.
280 * @param {V} val The value for which to check.
281 * @return {boolean} true If the map contains the value.
282 * @template K,V
283 */
284goog.object.containsValue = function(obj, val) {
285 for (var key in obj) {
286 if (obj[key] == val) {
287 return true;
288 }
289 }
290 return false;
291};
292
293
294/**
295 * Searches an object for an element that satisfies the given condition and
296 * returns its key.
297 * @param {Object.<K,V>} obj The object to search in.
298 * @param {function(this:T,V,string,Object.<K,V>):boolean} f The
299 * function to call for every element. Takes 3 arguments (the value,
300 * the key and the object) and should return a boolean.
301 * @param {T=} opt_this An optional "this" context for the function.
302 * @return {string|undefined} The key of an element for which the function
303 * returns true or undefined if no such element is found.
304 * @template T,K,V
305 */
306goog.object.findKey = function(obj, f, opt_this) {
307 for (var key in obj) {
308 if (f.call(opt_this, obj[key], key, obj)) {
309 return key;
310 }
311 }
312 return undefined;
313};
314
315
316/**
317 * Searches an object for an element that satisfies the given condition and
318 * returns its value.
319 * @param {Object.<K,V>} obj The object to search in.
320 * @param {function(this:T,V,string,Object.<K,V>):boolean} f The function
321 * to call for every element. Takes 3 arguments (the value, the key
322 * and the object) and should return a boolean.
323 * @param {T=} opt_this An optional "this" context for the function.
324 * @return {V} The value of an element for which the function returns true or
325 * undefined if no such element is found.
326 * @template T,K,V
327 */
328goog.object.findValue = function(obj, f, opt_this) {
329 var key = goog.object.findKey(obj, f, opt_this);
330 return key && obj[key];
331};
332
333
334/**
335 * Whether the object/map/hash is empty.
336 *
337 * @param {Object} obj The object to test.
338 * @return {boolean} true if obj is empty.
339 */
340goog.object.isEmpty = function(obj) {
341 for (var key in obj) {
342 return false;
343 }
344 return true;
345};
346
347
348/**
349 * Removes all key value pairs from the object/map/hash.
350 *
351 * @param {Object} obj The object to clear.
352 */
353goog.object.clear = function(obj) {
354 for (var i in obj) {
355 delete obj[i];
356 }
357};
358
359
360/**
361 * Removes a key-value pair based on the key.
362 *
363 * @param {Object} obj The object from which to remove the key.
364 * @param {*} key The key to remove.
365 * @return {boolean} Whether an element was removed.
366 */
367goog.object.remove = function(obj, key) {
368 var rv;
369 if ((rv = key in obj)) {
370 delete obj[key];
371 }
372 return rv;
373};
374
375
376/**
377 * Adds a key-value pair to the object. Throws an exception if the key is
378 * already in use. Use set if you want to change an existing pair.
379 *
380 * @param {Object.<K,V>} obj The object to which to add the key-value pair.
381 * @param {string} key The key to add.
382 * @param {V} val The value to add.
383 * @template K,V
384 */
385goog.object.add = function(obj, key, val) {
386 if (key in obj) {
387 throw Error('The object already contains the key "' + key + '"');
388 }
389 goog.object.set(obj, key, val);
390};
391
392
393/**
394 * Returns the value for the given key.
395 *
396 * @param {Object.<K,V>} obj The object from which to get the value.
397 * @param {string} key The key for which to get the value.
398 * @param {R=} opt_val The value to return if no item is found for the given
399 * key (default is undefined).
400 * @return {V|R|undefined} The value for the given key.
401 * @template K,V,R
402 */
403goog.object.get = function(obj, key, opt_val) {
404 if (key in obj) {
405 return obj[key];
406 }
407 return opt_val;
408};
409
410
411/**
412 * Adds a key-value pair to the object/map/hash.
413 *
414 * @param {Object.<K,V>} obj The object to which to add the key-value pair.
415 * @param {string} key The key to add.
416 * @param {V} value The value to add.
417 * @template K,V
418 */
419goog.object.set = function(obj, key, value) {
420 obj[key] = value;
421};
422
423
424/**
425 * Adds a key-value pair to the object/map/hash if it doesn't exist yet.
426 *
427 * @param {Object.<K,V>} obj The object to which to add the key-value pair.
428 * @param {string} key The key to add.
429 * @param {V} value The value to add if the key wasn't present.
430 * @return {V} The value of the entry at the end of the function.
431 * @template K,V
432 */
433goog.object.setIfUndefined = function(obj, key, value) {
434 return key in obj ? obj[key] : (obj[key] = value);
435};
436
437
438/**
439 * Compares two objects for equality using === on the values.
440 *
441 * @param {!Object.<K,V>} a
442 * @param {!Object.<K,V>} b
443 * @return {boolean}
444 * @template K,V
445 */
446goog.object.equals = function(a, b) {
447 if (!goog.array.equals(goog.object.getKeys(a), goog.object.getKeys(b))) {
448 return false;
449 }
450 for (var k in a) {
451 if (a[k] !== b[k]) {
452 return false;
453 }
454 }
455 return true;
456};
457
458
459/**
460 * Does a flat clone of the object.
461 *
462 * @param {Object.<K,V>} obj Object to clone.
463 * @return {!Object.<K,V>} Clone of the input object.
464 * @template K,V
465 */
466goog.object.clone = function(obj) {
467 // We cannot use the prototype trick because a lot of methods depend on where
468 // the actual key is set.
469
470 var res = {};
471 for (var key in obj) {
472 res[key] = obj[key];
473 }
474 return res;
475 // We could also use goog.mixin but I wanted this to be independent from that.
476};
477
478
479/**
480 * Clones a value. The input may be an Object, Array, or basic type. Objects and
481 * arrays will be cloned recursively.
482 *
483 * WARNINGS:
484 * <code>goog.object.unsafeClone</code> does not detect reference loops. Objects
485 * that refer to themselves will cause infinite recursion.
486 *
487 * <code>goog.object.unsafeClone</code> is unaware of unique identifiers, and
488 * copies UIDs created by <code>getUid</code> into cloned results.
489 *
490 * @param {*} obj The value to clone.
491 * @return {*} A clone of the input value.
492 */
493goog.object.unsafeClone = function(obj) {
494 var type = goog.typeOf(obj);
495 if (type == 'object' || type == 'array') {
496 if (obj.clone) {
497 return obj.clone();
498 }
499 var clone = type == 'array' ? [] : {};
500 for (var key in obj) {
501 clone[key] = goog.object.unsafeClone(obj[key]);
502 }
503 return clone;
504 }
505
506 return obj;
507};
508
509
510/**
511 * Returns a new object in which all the keys and values are interchanged
512 * (keys become values and values become keys). If multiple keys map to the
513 * same value, the chosen transposed value is implementation-dependent.
514 *
515 * @param {Object} obj The object to transpose.
516 * @return {!Object} The transposed object.
517 */
518goog.object.transpose = function(obj) {
519 var transposed = {};
520 for (var key in obj) {
521 transposed[obj[key]] = key;
522 }
523 return transposed;
524};
525
526
527/**
528 * The names of the fields that are defined on Object.prototype.
529 * @type {Array.<string>}
530 * @private
531 */
532goog.object.PROTOTYPE_FIELDS_ = [
533 'constructor',
534 'hasOwnProperty',
535 'isPrototypeOf',
536 'propertyIsEnumerable',
537 'toLocaleString',
538 'toString',
539 'valueOf'
540];
541
542
543/**
544 * Extends an object with another object.
545 * This operates 'in-place'; it does not create a new Object.
546 *
547 * Example:
548 * var o = {};
549 * goog.object.extend(o, {a: 0, b: 1});
550 * o; // {a: 0, b: 1}
551 * goog.object.extend(o, {b: 2, c: 3});
552 * o; // {a: 0, b: 2, c: 3}
553 *
554 * @param {Object} target The object to modify. Existing properties will be
555 * overwritten if they are also present in one of the objects in
556 * {@code var_args}.
557 * @param {...Object} var_args The objects from which values will be copied.
558 */
559goog.object.extend = function(target, var_args) {
560 var key, source;
561 for (var i = 1; i < arguments.length; i++) {
562 source = arguments[i];
563 for (key in source) {
564 target[key] = source[key];
565 }
566
567 // For IE the for-in-loop does not contain any properties that are not
568 // enumerable on the prototype object (for example isPrototypeOf from
569 // Object.prototype) and it will also not include 'replace' on objects that
570 // extend String and change 'replace' (not that it is common for anyone to
571 // extend anything except Object).
572
573 for (var j = 0; j < goog.object.PROTOTYPE_FIELDS_.length; j++) {
574 key = goog.object.PROTOTYPE_FIELDS_[j];
575 if (Object.prototype.hasOwnProperty.call(source, key)) {
576 target[key] = source[key];
577 }
578 }
579 }
580};
581
582
583/**
584 * Creates a new object built from the key-value pairs provided as arguments.
585 * @param {...*} var_args If only one argument is provided and it is an array
586 * then this is used as the arguments, otherwise even arguments are used as
587 * the property names and odd arguments are used as the property values.
588 * @return {!Object} The new object.
589 * @throws {Error} If there are uneven number of arguments or there is only one
590 * non array argument.
591 */
592goog.object.create = function(var_args) {
593 var argLength = arguments.length;
594 if (argLength == 1 && goog.isArray(arguments[0])) {
595 return goog.object.create.apply(null, arguments[0]);
596 }
597
598 if (argLength % 2) {
599 throw Error('Uneven number of arguments');
600 }
601
602 var rv = {};
603 for (var i = 0; i < argLength; i += 2) {
604 rv[arguments[i]] = arguments[i + 1];
605 }
606 return rv;
607};
608
609
610/**
611 * Creates a new object where the property names come from the arguments but
612 * the value is always set to true
613 * @param {...*} var_args If only one argument is provided and it is an array
614 * then this is used as the arguments, otherwise the arguments are used
615 * as the property names.
616 * @return {!Object} The new object.
617 */
618goog.object.createSet = function(var_args) {
619 var argLength = arguments.length;
620 if (argLength == 1 && goog.isArray(arguments[0])) {
621 return goog.object.createSet.apply(null, arguments[0]);
622 }
623
624 var rv = {};
625 for (var i = 0; i < argLength; i++) {
626 rv[arguments[i]] = true;
627 }
628 return rv;
629};
630
631
632/**
633 * Creates an immutable view of the underlying object, if the browser
634 * supports immutable objects.
635 *
636 * In default mode, writes to this view will fail silently. In strict mode,
637 * they will throw an error.
638 *
639 * @param {!Object.<K,V>} obj An object.
640 * @return {!Object.<K,V>} An immutable view of that object, or the
641 * original object if this browser does not support immutables.
642 * @template K,V
643 */
644goog.object.createImmutableView = function(obj) {
645 var result = obj;
646 if (Object.isFrozen && !Object.isFrozen(obj)) {
647 result = Object.create(obj);
648 Object.freeze(result);
649 }
650 return result;
651};
652
653
654/**
655 * @param {!Object} obj An object.
656 * @return {boolean} Whether this is an immutable view of the object.
657 */
658goog.object.isImmutableView = function(obj) {
659 return !!Object.isFrozen && Object.isFrozen(obj);
660};