master
Raw Download raw file
  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);