itsa-jsext
Version:
Extensions to native javascript-objects, all within the itsa_ namespace
575 lines (539 loc) • 20.6 kB
JavaScript
/**
*
* 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("./date");
var TYPES = {
"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
},
FUNCTION = "function",
// 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, parseDateString) {
var copy, i, len, value;
// Handle Array
if (Array.isArray(obj)) {
copy = [];
len = obj.length;
for (i=0; i<len; i++) {
value = obj[i];
copy[i] = (Object.itsa_isObject(value) || Array.isArray(value) || Date.itsa_isDate(value, parseDateString)) ? cloneObj(value, descriptors, parseDateString) : value;
}
return copy;
}
// Handle Date
if (Date.itsa_isDate(obj, parseDateString)) {
if (parseDateString && (typeof obj==="string")) {
copy = new Date(obj);
}
else {
copy = new Date();
copy.setTime(obj.getTime());
}
return copy;
}
// Handle Object
if (Object.itsa_isObject(obj)) {
return obj.itsa_deepClone(descriptors, null, parseDateString);
}
return obj;
},
valuesAreTheSame = function(value1, value2) {
var same, i, len;
// complex values need to be inspected differently:
if (Object.itsa_isObject(value1)) {
same = Object.itsa_isObject(value2) ? value1.itsa_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 (Date.itsa_isDate(value1)) {
same = Date.itsa_isDate(value2) ? (value1.getTime()===value2.getTime()) : false;
}
else {
same = (value1===value2);
}
return same;
},
deepCloneObj = function (source, target, descriptors, proto, parseDateString) {
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.itsa_isObject(value) || Array.isArray(value) || Date.itsa_isDate(value, parseDateString)) && ((typeof propDescriptor.get)!==FUNCTION) && ((typeof propDescriptor.set)!==FUNCTION)) {
m[key] = cloneObj(value, descriptors, parseDateString);
}
else {
m[key] = value;
}
}
else {
m[key] = (Object.itsa_isObject(value) || Array.isArray(value) || Date.itsa_isDate(value, parseDateString)) ? cloneObj(value, descriptors, parseDateString) : 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 itsa_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
*/
itsa_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 itsa_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*.
*/
itsa_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 itsa_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.
*/
itsa_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 itsa_keys
* @return {Array} Keys of the object
*/
itsa_keys: function () {
return Object.keys(this);
},
/**
* Checks whether the given property is a key: an enumerable property.
*
* @method itsa_hasKey
* @param property {String} the property to check for
* @return {Boolean} Keys of the object
*/
itsa_hasKey: function (property) {
return this.hasOwnProperty(property) && this.propertyIsEnumerable(property);
},
/**
* Returns the number of keys of the object
*
* @method itsa_size
* @param inclNonEnumerable {Boolean} wether to include non-enumeral members
* @return {Number} Number of items
*/
itsa_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 itsa_values
* @return {Array} values of the object
*/
itsa_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 itsa_isEmpty
* @return {Boolean} true if the object is empty
*/
itsa_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 itsa_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
*/
itsa_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 itsa_sameValue
* @param refObj {Object} the object to compare with
* @return {Boolean} whether both objects have the same value
*/
itsa_sameValue: function(refObj) {
var instance = this,
keys = Object.getOwnPropertyNames(instance),
l = keys.length,
i = -1,
same, key;
same = (l===refObj.itsa_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 itsa_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 [parseDateString=false] {Boolean} whether to automaticly parse stringified ISO Dates into Date objects
* @param [proto] {Object} Another prototype for the new object.
* @return {Object} deep-copy of the original
*/
itsa_deepClone: function (descriptors, proto, parseDateString) {
return deepCloneObj(this, null, descriptors, proto, parseDateString);
},
/**
* 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 itsa_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
*/
itsa_toArray: function(options) {
var newArray = [],
keyIdentifier = (options && options.key) || "key",
valueIdentifier = (options && options.value) || "value";
this.itsa_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 itsa_merge
* @param obj {Object} Object with the properties to be added to the original object
* @param [options] {Object}
* @param [options.force=false] {Boolean|'deep'}
* true ==> the properties in `obj` will override those of the same name in the original object
* false ==> the properties in `obj` will NOT be set if the name already exists in the original object
* 'deep' ==> the properties in `obj` will completely be deep-merged with the original object: both deep-proerties will endure. When
* both `obj` and the original object have the same `simple-type`-property, the `obj` its proerty will be used
* @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
*/
itsa_merge: function (obj, options) {
var instance = this,
i = -1,
deepForce, keys, l, key, force, replace, descriptors, propDescriptor;
if (!Object.itsa_isObject(obj)) {
return instance;
}
options || (options={});
keys = options.full ? Object.getOwnPropertyNames(obj) : Object.keys(obj);
l = keys.length;
force = options.force;
deepForce = (force==="deep");
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 (deepForce && Object.itsa_isObject(instance[key]) && Object.itsa_isObject(obj[key])) {
instance[key].itsa_merge(obj[key], options);
}
else {
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 itsa_defineData
* @param obj {Object} the Object that holds the new properties.
* @param [clone=false] {Boolean} whether the properties should be cloned
* @chainable
*/
itsa_defineData: function(obj, clone) {
var thisObj = this;
thisObj.itsa_emptyObject();
if (clone) {
deepCloneObj(obj, thisObj, true);
}
else {
thisObj.itsa_merge(obj);
}
return thisObj;
},
/**
* Empties the Object by deleting all its own properties (also non-enumerable).
*
* @method itsa_emptyObject
* @chainable
*/
itsa_emptyObject: 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 itsa_isObject
* @static
* @return {Boolean} true if the object is empty
*/
Object.itsa_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 && (!Promise || (!(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.itsa_merge(config, defaultConfig);
* }
*
* @method itsa_merge
* @static
* @param obj* {Object} Objects whose properties are to be merged
* @return {Object} new object with the properties merged in.
*/
Object.itsa_merge = function() {
var m = {};
Array.prototype.forEach.call(arguments, function (obj) {
if (obj) m.itsa_merge(obj);
});
return m;
};
/**
* Returns a new object with the prototype specified by `proto`.
*
*
* @method itsa_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.
* @param [parseDateString=false] {Boolean} whether to automaticly parse stringified ISO Dates into Date objects
* @return {Object} new object with the prototype specified.
*/
Object.itsa_newProto = function(obj, proto, clone, parseDateString) {
return clone ? obj.itsa_deepClone(true, proto, parseDateString) : Object.create(proto).itsa_merge(obj, {force: true});
};
/**
* Creates a protected property on the object.
*
* @method itsa_protectedProp
* @static
*/
Object.itsa_protectedProp = function(obj, property, value) {
Object.defineProperty(obj, property, {
configurable: false,
enumerable: false,
writable: false,
value: value
});
};