/**
*
* Pollyfils for often used functionality for Objects
*
* <i>Copyright (c) 2014 ITSA - https://github.com/itsa</i>
* New BSD License - http://choosealicense.com/licenses/bsd-3-clause/
*
* @module js-ext
* @submodule lib/object.js
* @class Object
*
*/
"use strict";
require('polyfill/polyfill-base.js');
require('polyfill/lib/promise.js'); // need promises
var createHashMap = require('js-ext/extra/hashmap.js').createMap,
TYPES = createHashMap({
'undefined' : true,
'number' : true,
'boolean' : true,
'string' : true,
'[object Function]' : true,
'[object RegExp]' : true,
'[object Array]' : true,
'[object Date]' : true,
'[object Error]' : true,
'[object Blob]' : true,
'[object Promise]' : true // DOES NOT WORK in all browsers
}),
// Define configurable, writable and non-enumerable props
// if they don't exist.
defineProperty = function (object, name, method, force) {
if (!force && (name in object)) {
return;
}
Object.defineProperty(object, name, {
configurable: true,
enumerable: false,
writable: true,
value: method
});
},
defineProperties = function (object, map, force) {
var names = Object.keys(map),
l = names.length,
i = -1,
name;
while (++i < l) {
name = names[i];
defineProperty(object, name, map[name], force);
}
},
cloneObj = function(obj, descriptors) {
var copy, i, len, value;
// Handle Array
if (obj instanceof Array) {
copy = [];
len = obj.length;
for (i=0; i<len; i++) {
value = obj[i];
copy[i] = (Object.isObject(value) || Array.isArray(value)) ? cloneObj(value, descriptors) : value;
}
return copy;
}
// Handle Date
if (obj instanceof Date) {
copy = new Date();
copy.setTime(obj.getTime());
return copy;
}
// Handle Object
if (Object.isObject(obj)) {
return obj.deepClone(descriptors);
}
return obj;
},
valuesAreTheSame = function(value1, value2) {
var same, i, len;
// complex values need to be inspected differently:
if (Object.isObject(value1)) {
same = Object.isObject(value2) ? value1.sameValue(value2) : false;
}
else if (Array.isArray(value1)) {
if (Array.isArray(value2)) {
len = value1.length;
if (len===value2.length) {
same = true;
for (i=0; same && (i<len); i++) {
same = valuesAreTheSame(value1[i], value2[i]);
}
}
else {
same = false;
}
}
else {
same = false;
}
}
else if (value1 instanceof Date) {
same = (value2 instanceof Date) ? (value1.getTime()===value2.getTime()) : false;
}
else {
same = (value1===value2);
}
return same;
},
deepCloneObj = function (source, target, descriptors, proto) {
var m = target || Object.create(proto || Object.getPrototypeOf(source)),
keys = Object.getOwnPropertyNames(source),
l = keys.length,
i = -1,
key, value, propDescriptor;
// loop through the members:
while (++i < l) {
key = keys[i];
value = source[key];
if (descriptors) {
propDescriptor = Object.getOwnPropertyDescriptor(source, key);
if (propDescriptor.writable) {
Object.defineProperty(m, key, propDescriptor);
}
if ((Object.isObject(value) || Array.isArray(value)) && ((typeof propDescriptor.get)!=='function') && ((typeof propDescriptor.set)!=='function')) {
m[key] = cloneObj(value, descriptors);
}
else {
m[key] = value;
}
}
else {
m[key] = (Object.isObject(value) || Array.isArray(value)) ? cloneObj(value, descriptors) : value;
}
}
return m;
};
/**
* Pollyfils for often used functionality for objects
* @class Object
*/
defineProperties(Object.prototype, {
/**
* Loops through all properties in the object. Equivalent to Array.forEach.
* The callback is provided with the value of the property, the name of the property
* and a reference to the whole object itself.
* The context to run the callback in can be overriden, otherwise it is undefined.
*
* @method each
* @param fn {Function} Function to be executed on each item in the object. It will receive
* value {any} value of the property
* key {string} name of the property
* obj {Object} the whole of the object
* @chainable
*/
each: function (fn, context) {
var obj = this,
keys = Object.keys(obj),
l = keys.length,
i = -1,
key;
while (++i < l) {
key = keys[i];
fn.call(context || obj, obj[key], key, obj);
}
return obj;
},
/**
* Loops through the properties in an object until the callback function returns *truish*.
* The callback is provided with the value of the property, the name of the property
* and a reference to the whole object itself.
* The order in which the elements are visited is not predictable.
* The context to run the callback in can be overriden, otherwise it is undefined.
*
* @method some
* @param fn {Function} Function to be executed on each item in the object. It will receive
* value {any} value of the property
* key {string} name of the property
* obj {Object} the whole of the object
* @return {Boolean} true if the loop was interrupted by the callback function returning *truish*.
*/
some: function (fn, context) {
var keys = Object.keys(this),
l = keys.length,
i = -1,
key;
while (++i < l) {
key = keys[i];
if (fn.call(context || this, this[key], key, this)) {
return true;
}
}
return false;
},
/**
* Loops through the properties in an object until the callback assembling a new object
* with its properties set to the values returned by the callback function.
* If the callback function returns `undefined` the property will not be copied to the new object.
* The resulting object will have the same keys as the original, except for those where the callback
* returned `undefined` which will have dissapeared.
* The callback is provided with the value of the property, the name of the property
* and a reference to the whole object itself.
* The context to run the callback in can be overriden, otherwise it is undefined.
*
* @method map
* @param fn {Function} Function to be executed on each item in the object. It will receive
* value {any} value of the property
* key {string} name of the property
* obj {Object} the whole of the object
* @return {Object} The new object with its properties set to the values returned by the callback function.
*/
map: function (fn, context) {
var keys = Object.keys(this),
l = keys.length,
i = -1,
m = {},
val, key;
while (++i < l) {
key = keys[i];
val = fn.call(context, this[key], key, this);
if (val !== undefined) {
m[key] = val;
}
}
return m;
},
/**
* Returns the keys of the object: the enumerable properties.
*
* @method keys
* @return {Array} Keys of the object
*/
keys: function () {
return Object.keys(this);
},
/**
* Checks whether the given property is a key: an enumerable property.
*
* @method hasKey
* @param property {String} the property to check for
* @return {Boolean} Keys of the object
*/
hasKey: function (property) {
return this.hasOwnProperty(property) && this.propertyIsEnumerable(property);
},
/**
* Returns the number of keys of the object
*
* @method size
* @param inclNonEnumerable {Boolean} wether to include non-enumeral members
* @return {Number} Number of items
*/
size: function (inclNonEnumerable) {
return inclNonEnumerable ? Object.getOwnPropertyNames(this).length : Object.keys(this).length;
},
/**
* Loops through the object collection the values of all its properties.
* It is the counterpart of the [`keys`](#method_keys).
*
* @method values
* @return {Array} values of the object
*/
values: function () {
var keys = Object.keys(this),
i = -1,
len = keys.length,
values = [];
while (++i < len) {
values.push(this[keys[i]]);
}
return values;
},
/**
* Returns true if the object has no own members
*
* @method isEmpty
* @return {Boolean} true if the object is empty
*/
isEmpty: function () {
for (var key in this) {
if (this.hasOwnProperty(key)) return false;
}
return true;
},
/**
* Returns a shallow copy of the object.
* It does not clone objects within the object, it does a simple, shallow clone.
* Fast, mostly useful for plain hash maps.
*
* @method shallowClone
* @param [options.descriptors=false] {Boolean} If true, the full descriptors will be set. This takes more time, but avoids any info to be lost.
* @return {Object} shallow copy of the original
*/
shallowClone: function (descriptors) {
var instance = this,
m = Object.create(Object.getPrototypeOf(instance)),
keys = Object.getOwnPropertyNames(instance),
l = keys.length,
i = -1,
key, propDescriptor;
while (++i < l) {
key = keys[i];
if (descriptors) {
propDescriptor = Object.getOwnPropertyDescriptor(instance, key);
if (!propDescriptor.writable) {
m[key] = instance[key];
}
else {
Object.defineProperty(m, key, propDescriptor);
}
}
else {
m[key] = instance[key];
}
}
return m;
},
/**
* Compares this object with the reference-object whether they have the same value.
* Not by reference, but their content as simple types.
*
* Compares both JSON.stringify objects
*
* @method sameValue
* @param refObj {Object} the object to compare with
* @return {Boolean} whether both objects have the same value
*/
sameValue: function(refObj) {
var instance = this,
keys = Object.getOwnPropertyNames(instance),
l = keys.length,
i = -1,
same, key;
same = (l===refObj.size(true));
// loop through the members:
while (same && (++i < l)) {
key = keys[i];
same = refObj.hasOwnProperty(key) ? valuesAreTheSame(instance[key], refObj[key]) : false;
}
return same;
},
/**
* Returns a deep copy of the object.
* Only handles members of primary types, Dates, Arrays and Objects.
* Will clone all the properties, also the non-enumerable.
*
* @method deepClone
* @param [descriptors=false] {Boolean} If true, the full descriptors will be set. This takes more time, but avoids any info to be lost.
* @param [proto] {Object} Another prototype for the new object.
* @return {Object} deep-copy of the original
*/
deepClone: function (descriptors, proto) {
return deepCloneObj(this, null, descriptors, proto);
},
/**
* Transforms the object into an array with 'key/value' objects
*
* @example
* {country: 'USA', Continent: 'North America'} --> [{key: 'country', value: 'USA'}, {key: 'Continent', value: 'North America'}]
*
* @method toArray
* @param [options] {Object}
* @param [options.key] {String} to overrule the default `key`-property-name
* @param [options.value] {String} to overrule the default `value`-property-name
* @return {Array} the transformed Array-representation of the object
*/
toArray: function(options) {
var newArray = [],
keyIdentifier = (options && options.key) || 'key',
valueIdentifier = (options && options.value) || 'value';
this.each(function(value, key) {
var obj = {};
obj[keyIdentifier] = key;
obj[valueIdentifier] = value;
newArray[newArray.length] = obj;
});
return newArray;
},
/**
* Merges into this object the properties of the given object.
* If the second argument is true, the properties on the source object will be overwritten
* by those of the second object of the same name, otherwise, they are preserved.
*
* @method merge
* @param obj {Object} Object with the properties to be added to the original object
* @param [options] {Object}
* @param [options.force=false] {Boolean} If true, the properties in `obj` will override those of the same name
* in the original object
* @param [options.full=false] {Boolean} If true, also any non-enumerable properties will be merged
* @param [options.replace=false] {Boolean} If true, only properties that already exist on the instance will be merged (forced replaced). No need to set force as well.
* @param [options.descriptors=false] {Boolean} If true, the full descriptors will be set. This takes more time, but avoids any info to be lost.
* @chainable
*/
merge: function (obj, options) {
var instance = this,
i = -1,
keys, l, key, force, replace, descriptors, propDescriptor;
if (!Object.isObject(obj)) {
return instance;
}
options || (options={});
keys = options.full ? Object.getOwnPropertyNames(obj) : Object.keys(obj);
l = keys.length;
force = options.force;
replace = options.replace;
descriptors = options.descriptors;
// we cannot use obj.each --> obj might be an object defined through Object.create(null) and missing Object.prototype!
while (++i < l) {
key = keys[i];
if ((force && !replace) || (!replace && !(key in instance)) || (replace && (key in instance))) {
if (descriptors) {
propDescriptor = Object.getOwnPropertyDescriptor(obj, key);
if (!propDescriptor.writable) {
instance[key] = obj[key];
}
else {
Object.defineProperty(instance, key, propDescriptor);
}
}
else {
instance[key] = obj[key];
}
}
}
return instance;
},
/**
* Sets the properties of `obj` to the instance. This will redefine the object, while remaining the instance.
* This way, external references to the object-instance remain valid.
*
* @method defineData
* @param obj {Object} the Object that holds the new properties.
* @param [clone=false] {Boolean} whether the properties should be cloned
* @chainable
*/
defineData: function(obj, clone) {
var thisObj = this;
thisObj.empty();
if (clone) {
deepCloneObj(obj, thisObj, true);
}
else {
thisObj.merge(obj);
}
return thisObj;
},
/**
* Empties the Object by deleting all its own properties (also non-enumerable).
*
* @method empty
* @chainable
*/
empty: function() {
var thisObj = this,
props = Object.getOwnPropertyNames(thisObj),
len = props.length,
i;
for (i=0; i<len; i++) {
delete thisObj[props[i]];
}
return thisObj;
}
});
/**
* Returns true if the item is an object, but no Array, Function, RegExp, Date or Error object
*
* @method isObject
* @static
* @return {Boolean} true if the object is empty
*/
Object.isObject = function (item) {
// cautious: some browsers detect Promises as [object Object] --> we always need to check instance of :(
return !!(!TYPES[typeof item] && !TYPES[({}.toString).call(item)] && item && (!(item instanceof Promise)));
};
/**
* Returns a new object resulting of merging the properties of the given objects.
* The copying is shallow, complex properties will reference the very same object.
* Properties in later objects do **not overwrite** properties of the same name in earlier objects.
* If any of the objects is missing, it will be skiped.
*
* @example
*
* var foo = function (config) {
* config = Object.merge(config, defaultConfig);
* }
*
* @method merge
* @static
* @param obj* {Object} Objects whose properties are to be merged
* @return {Object} new object with the properties merged in.
*/
Object.merge = function() {
var m = {};
Array.prototype.forEach.call(arguments, function (obj) {
if (obj) m.merge(obj);
});
return m;
};
/**
* Returns a new object with the prototype specified by `proto`.
*
*
* @method newProto
* @static
* @param obj {Object} source Object
* @param proto {Object} Object that should serve as prototype
* @param [clone=false] {Boolean} whether the sourceobject should be deep-cloned. When false, the properties will be merged.
* @return {Object} new object with the prototype specified.
*/
Object.newProto = function(obj, proto, clone) {
return clone ? obj.deepClone(true, proto) : Object.create(proto).merge(obj, {force: true});
};
/**
* Creates a protected property on the object.
*
* @method protectedProp
* @static
*/
Object.protectedProp = function(obj, property, value) {
Object.defineProperty(obj, property, {
configurable: false,
enumerable: false,
writable: false,
value: value
});
};