/*** * @package Object * @dependency core * @description Object manipulation, type checking (isNumber, isString, ...), extended objects with hash-like methods available as instance methods. * * Much thanks to kangax for his informative aricle about how problems with instanceof and constructor * http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/ * ***/ var ObjectTypeMethods = 'isObject,isNaN'.split(','); var ObjectHashMethods = 'keys,values,each,merge,clone,equal,watch,tap,has'.split(','); function setParamsObject(obj, param, value, deep) { var reg = /^(.+?)(\[.*\])$/, paramIsArray, match, allKeys, key; if(deep !== false && (match = param.match(reg))) { key = match[1]; allKeys = match[2].replace(/^\[|\]$/g, '').split(']['); allKeys.forEach(function(k) { paramIsArray = !k || k.match(/^\d+$/); if(!key && isArray(obj)) key = obj.length; if(!obj[key]) { obj[key] = paramIsArray ? [] : {}; } obj = obj[key]; key = k; }); if(!key && paramIsArray) key = obj.length.toString(); setParamsObject(obj, key, value); } else if(value.match(/^[+-]?\d+(\.\d+)?$/)) { obj[param] = parseFloat(value); } else if(value === 'true') { obj[param] = true; } else if(value === 'false') { obj[param] = false; } else { obj[param] = value; } } /*** * @method Object.is[Type]() * @returns Boolean * @short Returns true if is an object of that type. * @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". * * @set * isArray * isObject * isBoolean * isDate * isFunction * isNaN * isNumber * isString * isRegExp * * @example * * Object.isArray([1,2,3]) -> true * Object.isDate(3) -> false * Object.isRegExp(/wasabi/) -> true * Object.isObject({ broken:'wear' }) -> true * ***/ function buildTypeMethods() { extendSimilar(object, false, false, ClassNames, function(methods, name) { var method = 'is' + name; ObjectTypeMethods.push(method); methods[method] = function(obj) { return className(obj) === '[object '+name+']'; } }); } function buildObjectExtend() { extend(object, false, function(){ return arguments.length === 0; }, { 'extend': function() { buildObjectInstanceMethods(ObjectTypeMethods.concat(ObjectHashMethods), object); } }); } extend(object, false, true, { /*** * @method watch(, , ) * @returns Nothing * @short Watches a property of and runs when it changes. * @extra is passed three arguments: the property , 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%. This notably includes IE 8 and below, and Opera. 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. * @example * * Object.watch({ foo: 'bar' }, 'foo', function(prop, oldVal, newVal) { * // Will be run when the property 'foo' is set on the object. * }); * Object.extended().watch({ foo: 'bar' }, 'foo', function(prop, oldVal, newVal) { * // Will be run when the property 'foo' is set on the object. * }); * ***/ 'watch': function(obj, prop, fn) { if(!definePropertySupport) return; var value = obj[prop]; object.defineProperty(obj, prop, { 'enumerable' : true, 'configurable': true, 'get': function() { return value; }, 'set': function(to) { value = fn.call(obj, prop, value, to); } }); } }); extend(object, false, function(arg1, arg2) { return isFunction(arg2); }, { /*** * @method keys(, [fn]) * @returns Array * @short Returns an array containing the keys in . Optionally calls [fn] for each key. * @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. * @example * * Object.keys({ broken: 'wear' }) -> ['broken'] * Object.keys({ broken: 'wear' }, function(key, value) { * // Called once for each key. * }); * Object.extended({ broken: 'wear' }).keys() -> ['broken'] * ***/ 'keys': function(obj, fn) { var keys = object.keys(obj); keys.forEach(function(key) { fn.call(obj, key, obj[key]); }); return keys; } }); extend(object, false, false, { 'isObject': function(obj) { return isObject(obj); }, 'isNaN': function(obj) { // This is only true of NaN return isNumber(obj) && obj.valueOf() !== obj.valueOf(); }, /*** * @method equal(, ) * @returns Boolean * @short Returns true if and are equal. * @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. * @example * * Object.equal({a:2}, {a:2}) -> true * Object.equal({a:2}, {a:3}) -> false * Object.extended({a:2}).equals({a:3}) -> false * ***/ 'equal': function(a, b) { return isEqual(a, b); }, /*** * @method Object.extended( = {}) * @returns Extended object * @short Creates a new object, equivalent to %new Object()% or %{}%, but with extended methods. * @extra See extended objects for more. * @example * * Object.extended() * Object.extended({ happy:true, pappy:false }).keys() -> ['happy','pappy'] * Object.extended({ happy:true, pappy:false }).values() -> [true, false] * ***/ 'extended': function(obj) { return new Hash(obj); }, /*** * @method merge(, , [deep] = false, [resolve] = true) * @returns Merged object * @short Merges all the properties of into . * @extra Merges are shallow unless [deep] is %true%. Properties of 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 . 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. * @example * * Object.merge({a:1},{b:2}) -> { a:1, b:2 } * Object.merge({a:1},{a:2}, false, false) -> { a:1 } + Object.merge({a:1},{a:2}, false, function(key, a, b) { * return a + b; * }); -> { a:3 } * Object.extended({a:1}).merge({b:2}) -> { a:1, b:2 } * ***/ 'merge': function(target, source, deep, resolve) { var key, val; // Strings cannot be reliably merged thanks to // their properties not being enumerable in < IE8. if(target && typeof source != 'string') { for(key in source) { if(!hasOwnProperty(source, key) || !target) continue; val = source[key]; // Conflict! if(isDefined(target[key])) { // Do not merge. if(resolve === false) { continue; } // Use the result of the callback as the result. if(isFunction(resolve)) { val = resolve.call(source, key, target[key], source[key]) } } // Deep merging. if(deep === true && val && isObjectPrimitive(val)) { if(isDate(val)) { val = new date(val.getTime()); } else if(isRegExp(val)) { val = new regexp(val.source, getRegExpFlags(val)); } else { if(!target[key]) target[key] = array.isArray(val) ? [] : {}; object.merge(target[key], source[key], deep, resolve); continue; } } target[key] = val; } } return target; }, /*** * @method values(, [fn]) * @returns Array * @short Returns an array containing the values in . Optionally calls [fn] for each value. * @extra Returned values are in no particular order. %values% is available as an instance method on extended objects. * @example * * Object.values({ broken: 'wear' }) -> ['wear'] * Object.values({ broken: 'wear' }, function(value) { * // Called once for each value. * }); * Object.extended({ broken: 'wear' }).values() -> ['wear'] * ***/ 'values': function(obj, fn) { var values = []; iterateOverObject(obj, function(k,v) { values.push(v); if(fn) fn.call(obj,v); }); return values; }, /*** * @method clone( = {}, [deep] = false) * @returns Cloned object * @short Creates a clone (copy) of . * @extra Default is a shallow clone, unless [deep] is true. %clone% is available as an instance method on extended objects. * @example * * Object.clone({foo:'bar'}) -> { foo: 'bar' } * Object.clone() -> {} * Object.extended({foo:'bar'}).clone() -> { foo: 'bar' } * ***/ 'clone': function(obj, deep) { if(!isObjectPrimitive(obj)) return obj; if(array.isArray(obj)) return obj.concat(); var target = obj instanceof Hash ? new Hash() : {}; return object.merge(target, obj, deep); }, /*** * @method Object.fromQueryString(, [deep] = true) * @returns Object * @short Converts the query string of a URL into an object. * @extra If [deep] is %false%, conversion will only accept shallow params (ie. no object or arrays with %[]% syntax) as these are not universally supported. * @example * * Object.fromQueryString('foo=bar&broken=wear') -> { foo: 'bar', broken: 'wear' } * Object.fromQueryString('foo[]=1&foo[]=2') -> { foo: [1,2] } * ***/ 'fromQueryString': function(str, deep) { var result = object.extended(), split; str = str && str.toString ? str.toString() : ''; str.replace(/^.*?\?/, '').split('&').forEach(function(p) { var split = p.split('='); if(split.length !== 2) return; setParamsObject(result, split[0], decodeURIComponent(split[1]), deep); }); return result; }, /*** * @method tap(, ) * @returns Object * @short Runs and returns . * @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(). * @example * * Object.extend(); * [2,4,6].map(Math.exp).tap(function(arr) { * arr.pop() * }); * [2,4,6].map(Math.exp).tap('pop').map(Math.round); -> [7,55] * ***/ 'tap': function(obj, arg) { var fn = arg; if(!isFunction(arg)) { fn = function() { if(arg) obj[arg](); } } fn.call(obj, obj); return obj; }, /*** * @method has(, ) * @returns Boolean * @short Checks if has using hasOwnProperty from Object.prototype. * @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. * @example * * Object.has({ foo: 'bar' }, 'foo') -> true * Object.has({ foo: 'bar' }, 'baz') -> false * Object.has({ hasOwnProperty: true }, 'foo') -> false * ***/ 'has': function (obj, key) { return hasOwnProperty(obj, key); } }); buildTypeMethods(); buildObjectExtend(); buildObjectInstanceMethods(ObjectHashMethods, Hash);