sugar
Version:
A Javascript library for working with native objects.
395 lines (365 loc) • 14.1 kB
JavaScript
/***
* 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);