UNPKG

sugar

Version:

A Javascript library for working with native objects.

395 lines (365 loc) 14.1 kB
/*** * Object module * @dependency core * * 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,isEmpty,clone,equal,watch,tap,has'.split(','); function Hash(obj) { object.merge(this, obj); }; Hash.prototype.constructor = object; 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.]+$/)) { obj[param] = parseFloat(value); } else if(value === 'true') { obj[param] = true; } else if(value === 'false') { obj[param] = false; } else { obj[param] = value; } } /*** * @method is[Type](<obj>) * @returns Boolean * @short Returns true if <obj> 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". Type methods are available as instance methods on extended objects. * * @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 isClass(obj, name); } }); } function buildObjectExtend() { extend(object, false, function(){ return arguments.length === 0; }, { 'extend': function() { buildObjectInstanceMethods(ObjectTypeMethods.concat(ObjectHashMethods), object); } }); } extend(object, false, true, { /*** * @method watch(<obj>, <prop>, <fn>) * @returns Nothing * @short Watches a property of <obj> and runs <fn> when it changes. * @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%. 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(<obj>, [fn]) * @returns Array * @short Returns an array containing the keys in <obj>. 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); object.keys(obj).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(<a>, <b>) * @returns Boolean * @short Returns true if <a> and <b> 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 stringify(a) === stringify(b); }, /*** * @method Object.extended(<obj> = {}) * @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(<target>, <source>, [deep] = false, [resolve] = true) * @returns Merged object * @short Merges all the properties of <source> into <target>. * @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. * @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(<obj>, [fn]) * @returns Array * @short Returns an array containing the values in <obj>. 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 each(<obj>, [fn]) * @returns Object * @short Iterates over each property in <obj> calling [fn] on each iteration. * @extra %each% is available as an instance method on extended objects. * @example * * Object.each({ broken:'wear' }, function(key, value) { * // Iterates over each key/value pair. * }); * Object.extended({ broken:'wear' }).each(function(key, value) { * // Iterates over each key/value pair. * }); * ***/ 'each': function(obj, fn) { if(fn) { iterateOverObject(obj, function(k,v) { fn.call(obj, k, v, obj); }); } return obj; }, /*** * @method isEmpty(<obj>) * @returns Boolean * @short Returns true if <obj> is empty. * @extra %isEmpty% is available as an instance method on extended objects. * @example * * Object.isEmpty({}) -> true * Object.isEmpty({foo:'bar'}) -> false * Object.extended({foo:'bar'}).isEmpty() -> false * ***/ 'isEmpty': function(obj) { if(!isObjectPrimitive(obj)) return !(obj && obj.length > 0); return object.keys(obj).length == 0; }, /*** * @method clone(<obj> = {}, [deep] = false) * @returns Cloned object * @short Creates a clone (copy) of <obj>. * @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(<str>, [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() : ''; decodeURIComponent(str.replace(/^.*?\?/, '')).split('&').forEach(function(p) { var split = p.split('='); if(split.length !== 2) return; setParamsObject(result, split[0], split[1], deep); }); return result; }, /*** * @method tap(<obj>, <fn>) * @returns Object * @short Runs <fn> and returns <obj>. * @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.pop(); }).map(Math.round); -> [7,55] * [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); //transformArgument(obj, fn, obj, [obj]); return obj; }, /*** * @method has(<obj>, <key>) * @returns Boolean * @short Checks if <obj> has <key> 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);