master
1
2 'use strict';
3
4 /***
5 * @package Object
6 * @dependency core
7 * @description Object manipulation, type checking (isNumber, isString, ...), extended objects with hash-like methods available as instance methods.
8 *
9 * Much thanks to kangax for his informative aricle about how problems with instanceof and constructor
10 * http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
11 *
12 ***/
13
14 var ObjectTypeMethods = 'isObject,isNaN'.split(',');
15 var ObjectHashMethods = 'keys,values,select,reject,each,merge,clone,equal,watch,tap,has,toQueryString'.split(',');
16
17 function setParamsObject(obj, param, value, castBoolean) {
18 var reg = /^(.+?)(\[.*\])$/, paramIsArray, match, allKeys, key;
19 if(match = param.match(reg)) {
20 key = match[1];
21 allKeys = match[2].replace(/^\[|\]$/g, '').split('][');
22 allKeys.forEach(function(k) {
23 paramIsArray = !k || k.match(/^\d+$/);
24 if(!key && isArray(obj)) key = obj.length;
25 if(!hasOwnProperty(obj, key)) {
26 obj[key] = paramIsArray ? [] : {};
27 }
28 obj = obj[key];
29 key = k;
30 });
31 if(!key && paramIsArray) key = obj.length.toString();
32 setParamsObject(obj, key, value, castBoolean);
33 } else if(castBoolean && value === 'true') {
34 obj[param] = true;
35 } else if(castBoolean && value === 'false') {
36 obj[param] = false;
37 } else {
38 obj[param] = value;
39 }
40 }
41
42 function objectToQueryString(base, obj) {
43 var tmp;
44 // If a custom toString exists bail here and use that instead
45 if(isArray(obj) || (isObjectType(obj) && obj.toString === internalToString)) {
46 tmp = [];
47 iterateOverObject(obj, function(key, value) {
48 if(base) {
49 key = base + '[' + key + ']';
50 }
51 tmp.push(objectToQueryString(key, value));
52 });
53 return tmp.join('&');
54 } else {
55 if(!base) return '';
56 return sanitizeURIComponent(base) + '=' + (isDate(obj) ? obj.getTime() : sanitizeURIComponent(obj));
57 }
58 }
59
60 function sanitizeURIComponent(obj) {
61 // undefined, null, and NaN are represented as a blank string,
62 // while false and 0 are stringified. "+" is allowed in query string
63 return !obj && obj !== false && obj !== 0 ? '' : encodeURIComponent(obj).replace(/%20/g, '+');
64 }
65
66 function matchInObject(match, key, value) {
67 if(isRegExp(match)) {
68 return match.test(key);
69 } else if(isObjectType(match)) {
70 return match[key] === value;
71 } else {
72 return key === string(match);
73 }
74 }
75
76 function selectFromObject(obj, args, select) {
77 var match, result = obj instanceof Hash ? new Hash : {};
78 iterateOverObject(obj, function(key, value) {
79 match = false;
80 flattenedArgs(args, function(arg) {
81 if(matchInObject(arg, key, value)) {
82 match = true;
83 }
84 }, 1);
85 if(match === select) {
86 result[key] = value;
87 }
88 });
89 return result;
90 }
91
92
93 /***
94 * @method Object.is[Type](<obj>)
95 * @returns Boolean
96 * @short Returns true if <obj> is an object of that type.
97 * @extra %isObject% will return false on anything that is not an object literal, including instances of inherited classes. Note also that %isNaN% will ONLY return true if the object IS %NaN%. It does not mean the same as browser native %isNaN%, which returns true for anything that is "not a number".
98 *
99 * @set
100 * isArray
101 * isObject
102 * isBoolean
103 * isDate
104 * isFunction
105 * isNaN
106 * isNumber
107 * isString
108 * isRegExp
109 *
110 * @example
111 *
112 * Object.isArray([1,2,3]) -> true
113 * Object.isDate(3) -> false
114 * Object.isRegExp(/wasabi/) -> true
115 * Object.isObject({ broken:'wear' }) -> true
116 *
117 ***/
118 function buildTypeMethods() {
119 extendSimilar(object, false, true, ClassNames, function(methods, name) {
120 var method = 'is' + name;
121 ObjectTypeMethods.push(method);
122 methods[method] = typeChecks[name];
123 });
124 }
125
126 function buildObjectExtend() {
127 extend(object, false, function(){ return arguments.length === 0; }, {
128 'extend': function() {
129 var methods = ObjectTypeMethods.concat(ObjectHashMethods)
130 if(typeof EnumerableMethods !== 'undefined') {
131 methods = methods.concat(EnumerableMethods);
132 }
133 buildObjectInstanceMethods(methods, object);
134 }
135 });
136 }
137
138 extend(object, false, true, {
139 /***
140 * @method watch(<obj>, <prop>, <fn>)
141 * @returns Nothing
142 * @short Watches a property of <obj> and runs <fn> when it changes.
143 * @extra <fn> is passed three arguments: the property <prop>, the old value, and the new value. The return value of [fn] will be set as the new value. This method is useful for things such as validating or cleaning the value when it is set. Warning: this method WILL NOT work in browsers that don't support %Object.defineProperty% (IE 8 and below). This is the only method in Sugar that is not fully compatible with all browsers. %watch% is available as an instance method on extended objects.
144 * @example
145 *
146 * Object.watch({ foo: 'bar' }, 'foo', function(prop, oldVal, newVal) {
147 * // Will be run when the property 'foo' is set on the object.
148 * });
149 * Object.extended().watch({ foo: 'bar' }, 'foo', function(prop, oldVal, newVal) {
150 * // Will be run when the property 'foo' is set on the object.
151 * });
152 *
153 ***/
154 'watch': function(obj, prop, fn) {
155 if(!definePropertySupport) return;
156 var value = obj[prop];
157 object.defineProperty(obj, prop, {
158 'enumerable' : true,
159 'configurable': true,
160 'get': function() {
161 return value;
162 },
163 'set': function(to) {
164 value = fn.call(obj, prop, value, to);
165 }
166 });
167 }
168 });
169
170 extend(object, false, function() { return arguments.length > 1; }, {
171
172 /***
173 * @method keys(<obj>, [fn])
174 * @returns Array
175 * @short Returns an array containing the keys in <obj>. Optionally calls [fn] for each key.
176 * @extra This method is provided for browsers that don't support it natively, and additionally is enhanced to accept the callback [fn]. Returned keys are in no particular order. %keys% is available as an instance method on extended objects.
177 * @example
178 *
179 * Object.keys({ broken: 'wear' }) -> ['broken']
180 * Object.keys({ broken: 'wear' }, function(key, value) {
181 * // Called once for each key.
182 * });
183 * Object.extended({ broken: 'wear' }).keys() -> ['broken']
184 *
185 ***/
186 'keys': function(obj, fn) {
187 var keys = object.keys(obj);
188 keys.forEach(function(key) {
189 fn.call(obj, key, obj[key]);
190 });
191 return keys;
192 }
193
194 });
195
196 extend(object, false, true, {
197
198 'isObject': function(obj) {
199 return isPlainObject(obj);
200 },
201
202 'isNaN': function(obj) {
203 // This is only true of NaN
204 return isNumber(obj) && obj.valueOf() !== obj.valueOf();
205 },
206
207 /***
208 * @method equal(<a>, <b>)
209 * @returns Boolean
210 * @short Returns true if <a> and <b> are equal.
211 * @extra %equal% in Sugar is "egal", meaning the values are equal if they are "not observably distinguishable". Note that on extended objects the name is %equals% for readability.
212 * @example
213 *
214 * Object.equal({a:2}, {a:2}) -> true
215 * Object.equal({a:2}, {a:3}) -> false
216 * Object.extended({a:2}).equals({a:3}) -> false
217 *
218 ***/
219 'equal': function(a, b) {
220 return isEqual(a, b);
221 },
222
223 /***
224 * @method Object.extended(<obj> = {})
225 * @returns Extended object
226 * @short Creates a new object, equivalent to %new Object()% or %{}%, but with extended methods.
227 * @extra See extended objects for more.
228 * @example
229 *
230 * Object.extended()
231 * Object.extended({ happy:true, pappy:false }).keys() -> ['happy','pappy']
232 * Object.extended({ happy:true, pappy:false }).values() -> [true, false]
233 *
234 ***/
235 'extended': function(obj) {
236 return new Hash(obj);
237 },
238
239 /***
240 * @method merge(<target>, <source>, [deep] = false, [resolve] = true)
241 * @returns Merged object
242 * @short Merges all the properties of <source> into <target>.
243 * @extra Merges are shallow unless [deep] is %true%. Properties of <source> will win in the case of conflicts, unless [resolve] is %false%. [resolve] can also be a function that resolves the conflict. In this case it will be passed 3 arguments, %key%, %targetVal%, and %sourceVal%, with the context set to <source>. This will allow you to solve conflict any way you want, ie. adding two numbers together, etc. %merge% is available as an instance method on extended objects.
244 * @example
245 *
246 * Object.merge({a:1},{b:2}) -> { a:1, b:2 }
247 * Object.merge({a:1},{a:2}, false, false) -> { a:1 }
248 + Object.merge({a:1},{a:2}, false, function(key, a, b) {
249 * return a + b;
250 * }); -> { a:3 }
251 * Object.extended({a:1}).merge({b:2}) -> { a:1, b:2 }
252 *
253 ***/
254 'merge': function(target, source, deep, resolve) {
255 var key, sourceIsObject, targetIsObject, sourceVal, targetVal, conflict, result;
256 // Strings cannot be reliably merged thanks to
257 // their properties not being enumerable in < IE8.
258 if(target && typeof source !== 'string') {
259 for(key in source) {
260 if(!hasOwnProperty(source, key) || !target) continue;
261 sourceVal = source[key];
262 targetVal = target[key];
263 conflict = isDefined(targetVal);
264 sourceIsObject = isObjectType(sourceVal);
265 targetIsObject = isObjectType(targetVal);
266 result = conflict && resolve === false ? targetVal : sourceVal;
267
268 if(conflict) {
269 if(isFunction(resolve)) {
270 // Use the result of the callback as the result.
271 result = resolve.call(source, key, targetVal, sourceVal)
272 }
273 }
274
275 // Going deep
276 if(deep && (sourceIsObject || targetIsObject)) {
277 if(isDate(sourceVal)) {
278 result = new date(sourceVal.getTime());
279 } else if(isRegExp(sourceVal)) {
280 result = new regexp(sourceVal.source, getRegExpFlags(sourceVal));
281 } else {
282 if(!targetIsObject) target[key] = array.isArray(sourceVal) ? [] : {};
283 object.merge(target[key], sourceVal, deep, resolve);
284 continue;
285 }
286 }
287 target[key] = result;
288 }
289 }
290 return target;
291 },
292
293 /***
294 * @method values(<obj>, [fn])
295 * @returns Array
296 * @short Returns an array containing the values in <obj>. Optionally calls [fn] for each value.
297 * @extra Returned values are in no particular order. %values% is available as an instance method on extended objects.
298 * @example
299 *
300 * Object.values({ broken: 'wear' }) -> ['wear']
301 * Object.values({ broken: 'wear' }, function(value) {
302 * // Called once for each value.
303 * });
304 * Object.extended({ broken: 'wear' }).values() -> ['wear']
305 *
306 ***/
307 'values': function(obj, fn) {
308 var values = [];
309 iterateOverObject(obj, function(k,v) {
310 values.push(v);
311 if(fn) fn.call(obj,v);
312 });
313 return values;
314 },
315
316 /***
317 * @method clone(<obj> = {}, [deep] = false)
318 * @returns Cloned object
319 * @short Creates a clone (copy) of <obj>.
320 * @extra Default is a shallow clone, unless [deep] is true. %clone% is available as an instance method on extended objects.
321 * @example
322 *
323 * Object.clone({foo:'bar'}) -> { foo: 'bar' }
324 * Object.clone() -> {}
325 * Object.extended({foo:'bar'}).clone() -> { foo: 'bar' }
326 *
327 ***/
328 'clone': function(obj, deep) {
329 var target, klass;
330 if(!isObjectType(obj)) {
331 return obj;
332 }
333 klass = className(obj);
334 if(isDate(obj, klass) && obj.clone) {
335 // Preserve internal UTC flag when applicable.
336 return obj.clone();
337 } else if(isDate(obj, klass) || isRegExp(obj, klass)) {
338 return new obj.constructor(obj);
339 } else if(obj instanceof Hash) {
340 target = new Hash;
341 } else if(isArray(obj, klass)) {
342 target = [];
343 } else if(isPlainObject(obj, klass)) {
344 target = {};
345 } else {
346 throw new TypeError('Clone must be a basic data type.');
347 }
348 return object.merge(target, obj, deep);
349 },
350
351 /***
352 * @method Object.fromQueryString(<str>, [booleans] = false)
353 * @returns Object
354 * @short Converts the query string of a URL into an object.
355 * @extra If [booleans] is true, then %"true"% and %"false"% will be cast into booleans. All other values, including numbers will remain their string values.
356 * @example
357 *
358 * Object.fromQueryString('foo=bar&broken=wear') -> { foo: 'bar', broken: 'wear' }
359 * Object.fromQueryString('foo[]=1&foo[]=2') -> { foo: ['1','2'] }
360 * Object.fromQueryString('foo=true', true) -> { foo: true }
361 *
362 ***/
363 'fromQueryString': function(str, castBoolean) {
364 var result = object.extended(), split;
365 str = str && str.toString ? str.toString() : '';
366 str.replace(/^.*?\?/, '').split('&').forEach(function(p) {
367 var split = p.split('=');
368 if(split.length !== 2) return;
369 setParamsObject(result, split[0], decodeURIComponent(split[1]), castBoolean);
370 });
371 return result;
372 },
373
374 /***
375 * @method Object.toQueryString(<obj>, [namespace] = null)
376 * @returns Object
377 * @short Converts the object into a query string.
378 * @extra Accepts deep nested objects and arrays. If [namespace] is passed, it will be prefixed to all param names.
379 * @example
380 *
381 * Object.toQueryString({foo:'bar'}) -> 'foo=bar'
382 * Object.toQueryString({foo:['a','b','c']}) -> 'foo[0]=a&foo[1]=b&foo[2]=c'
383 * Object.toQueryString({name:'Bob'}, 'user') -> 'user[name]=Bob'
384 *
385 ***/
386 'toQueryString': function(obj, namespace) {
387 return objectToQueryString(namespace, obj);
388 },
389
390 /***
391 * @method tap(<obj>, <fn>)
392 * @returns Object
393 * @short Runs <fn> and returns <obj>.
394 * @extra A string can also be used as a shortcut to a method. This method is used to run an intermediary function in the middle of method chaining. As a standalone method on the Object class it doesn't have too much use. The power of %tap% comes when using extended objects or modifying the Object prototype with Object.extend().
395 * @example
396 *
397 * Object.extend();
398 * [2,4,6].map(Math.exp).tap(function(arr) {
399 * arr.pop()
400 * });
401 * [2,4,6].map(Math.exp).tap('pop').map(Math.round); -> [7,55]
402 *
403 ***/
404 'tap': function(obj, arg) {
405 var fn = arg;
406 if(!isFunction(arg)) {
407 fn = function() {
408 if(arg) obj[arg]();
409 }
410 }
411 fn.call(obj, obj);
412 return obj;
413 },
414
415 /***
416 * @method has(<obj>, <key>)
417 * @returns Boolean
418 * @short Checks if <obj> has <key> using hasOwnProperty from Object.prototype.
419 * @extra This method is considered safer than %Object#hasOwnProperty% when using objects as hashes. See http://www.devthought.com/2012/01/18/an-object-is-not-a-hash/ for more.
420 * @example
421 *
422 * Object.has({ foo: 'bar' }, 'foo') -> true
423 * Object.has({ foo: 'bar' }, 'baz') -> false
424 * Object.has({ hasOwnProperty: true }, 'foo') -> false
425 *
426 ***/
427 'has': function (obj, key) {
428 return hasOwnProperty(obj, key);
429 },
430
431 /***
432 * @method select(<obj>, <find>, ...)
433 * @returns Object
434 * @short Builds a new object containing the values specified in <find>.
435 * @extra When <find> is a string, that single key will be selected. It can also be a regex, selecting any key that matches, or an object which will match if the key also exists in that object, effectively doing an "intersect" operation on that object. Multiple selections may also be passed as an array or directly as enumerated arguments. %select% is available as an instance method on extended objects.
436 * @example
437 *
438 * Object.select({a:1,b:2}, 'a') -> {a:1}
439 * Object.select({a:1,b:2}, /[a-z]/) -> {a:1,ba:2}
440 * Object.select({a:1,b:2}, {a:1}) -> {a:1}
441 * Object.select({a:1,b:2}, 'a', 'b') -> {a:1,b:2}
442 * Object.select({a:1,b:2}, ['a', 'b']) -> {a:1,b:2}
443 *
444 ***/
445 'select': function (obj) {
446 return selectFromObject(obj, arguments, true);
447 },
448
449 /***
450 * @method reject(<obj>, <find>, ...)
451 * @returns Object
452 * @short Builds a new object containing all values except those specified in <find>.
453 * @extra When <find> is a string, that single key will be rejected. It can also be a regex, rejecting any key that matches, or an object which will match if the key also exists in that object, effectively "subtracting" that object. Multiple selections may also be passed as an array or directly as enumerated arguments. %reject% is available as an instance method on extended objects.
454 * @example
455 *
456 * Object.reject({a:1,b:2}, 'a') -> {b:2}
457 * Object.reject({a:1,b:2}, /[a-z]/) -> {}
458 * Object.reject({a:1,b:2}, {a:1}) -> {b:2}
459 * Object.reject({a:1,b:2}, 'a', 'b') -> {}
460 * Object.reject({a:1,b:2}, ['a', 'b']) -> {}
461 *
462 ***/
463 'reject': function (obj) {
464 return selectFromObject(obj, arguments, false);
465 }
466
467 });
468
469
470 buildTypeMethods();
471 buildObjectExtend();
472 buildObjectInstanceMethods(ObjectHashMethods, Hash);