/** * @module mixing */ /*jshint latedef:nofunc*/ var defaultSettings; if (! Array.isArray) { Array.isArray = function(obj) { return Object.prototype.toString.call(obj) === "[object Array]"; }; } function prepareFieldList(fieldList, value) { var map, nI, regexp, sType; if (Array.isArray(fieldList)) { if (fieldList.length > 0) { map = {}; nI = fieldList.length; do { map[ fieldList[--nI] ] = value || null; } while(nI); } } else { sType = typeof fieldList; if (sType === "string" || sType === "symbol") { map = {}; map[fieldList] = value || null; } else if (sType === "object") { if (fieldList instanceof RegExp) { regexp = fieldList; } else { map = fieldList; } } } return {map: map, regexp: regexp}; } function copy(destination, source, propName, settings) { /*jshint laxbreak:true*/ var propValue = source[propName], sPropString = propName.toString(), bFuncProp, change, otherNameMap, sType, value; function getParam() { return { field: propName, value: propValue, targetValue: destination[propName], target: destination, source: source }; } if ((! settings.ownProperty || source.hasOwnProperty(propName)) && (! settings.copyMap || (propName in settings.copyMap)) && (! settings.copyRegExp || settings.copyRegExp.test(sPropString)) && (! settings.exceptions || ! settings.exceptions[propName]) && (! settings.exceptRegExp || ! settings.exceptRegExp.test(sPropString)) && (! settings.filter || settings.filter.call(null, getParam())) /* jshint -W122 */ && (! settings.filterRegExp || settings.filterRegExp.test(typeof propValue === "symbol" ? propValue.toString() : propValue))) { /* jshint +W122 */ otherNameMap = settings.otherNameMap; if (otherNameMap && (propName in otherNameMap)) { propName = otherNameMap[propName]; } sType = typeof propValue; // If recursive mode and field's value is an object if (settings.recursive && propValue && sType === "object" && (value = destination[propName]) && typeof value === "object" && (! Array.isArray(propValue) || settings.mixFromArray) && (! Array.isArray(value) || settings.mixToArray)) { mixing(value, propValue, settings.mixFromArray ? mixing({oneSource: true}, settings) : settings); } else { bFuncProp = (sType === "function"); if ((! bFuncProp || settings.copyFunc) && (! (propName in destination) || ((value = settings.overwrite) && ( typeof value !== "function" || value(getParam()) ) && ( ! (value instanceof RegExp) || value.test(propName) ) ))) { if (settings.changeFunc) { propValue = settings.changeFunc.call(null, getParam()); } else if ((change = settings.change) && (propName in change)) { propValue = change[propName]; } if (bFuncProp && settings.funcToProto) { destination.constructor.prototype[propName] = propValue; } else { destination[propName] = propValue; } } } } } /** * Copy/add all fields and functions from source objects into the target object. * As a result the target object may be modified. * * @param {Object | Function} destination * The target object into which fields and functions will be copied. * @param {Array | Object} source * Array of source objects or just one object whose contents will be copied. * If a source is a falsy value (e.g. `null` or `undefined`), the source will be skipped. * @param {Object} [settings] * Operation settings. Fields are names of settings, their values are the corresponding values of settings. * The following settings are being supported. * <table> * <tr> * <th>Name</th><th>Type</th><th>Default value</th><th>Description</th> * </tr> * <tr> * <td>`copyFunc`</td> * <td>`Boolean`</td> * <td>`true`</td> * <td>Should functions be copied?</td> * </tr> * <tr> * <td>`funcToProto`</td> * <td>`Boolean`</td> * <td>`false`</td> * <td> * Should functions be copied into `prototype` of the target object's `constructor` * (i.e. into `destination.constructor.prototype`)? * <br> * If `false` then functions will be copied directly into the target object. * </td> * </tr> * <tr> * <td>`processSymbol`</td> * <td>`Boolean`</td> * <td>`true`</td> * <td>Should symbol property keys (i.e. fields whose name is a symbol) be processed?</td> * </tr> * <tr> * <td>`overwrite`</td> * <td>`Boolean | Function | RegExp`</td> * <td>`false`</td> * <td> * Specifies whether a field/function should be overwritten when it exists in the target object. * <br> * If `true` then any existent field will be overwritten in the target object. * <br> * Function or regular expression can be used to select fields that should be overwritten. * <br> * If a regular expression is passed, only those fields will be overwritten * whose names are matching the regular expression. * <br> * If specified function returns `true` for a field, * the field will be overwritten in the target object. * An object with contextual data is passed into the function (see details below). * </td> * </tr> * <tr> * <td>`recursive`</td> * <td>`Boolean`</td> * <td>`false`</td> * <td> * Should this function be called recursively when field's value of the target and source object is an object? * <br> * If `true` then object fields from the target and source objects will be mixed by using this function * with the same settings. * <br> * This option has no dependency with `overwrite` setting and has priority over it. * </td> * </tr> * <tr> * <td>`mixFromArray`</td> * <td>`Boolean`</td> * <td>`false`</td> * <td> * Should contents of a field of the source object be copied when the field's value is an array? * <br> * Will be used only when `recursive` setting has `true` value. * </td> * </tr> * <tr> * <td>`mixToArray`</td> * <td>`Boolean`</td> * <td>`false`</td> * <td> * Should contents of a field of the source object be copied into a field of the target object * when the latest field's value is an array? * <br> * Will be used only when `recursive` setting has `true` value. * </td> * </tr> * <tr> * <td>`mixArray`</td> * <td>`Boolean`</td> * <td>`false`</td> * <td> * Sets default value for `mixFromArray` and `mixToArray` settings. * </td> * </tr> * <tr> * <td>`oneSource`</td> * <td>`Boolean`</td> * <td>`false`</td> * <td> * Indicates that array that is passed as `source` parameter should be interpreted * directly as copied object instead of list of source objects. * </td> * </tr> * <tr> * <td>`ownProperty`</td> * <td>`Boolean`</td> * <td>`false`</td> * <td>Should only own properties of the source object be copied in the target object?</td> * </tr> * <tr> * <td>`copy`</td> * <td>`Array | Object | RegExp | String | Symbol`</td> * <td>`""` (empty string)</td> * <td> * Array, object, regular expression or string/symbol that defines names of fields/functions that should be copied. * <br> * If an object is passed then his fields determine copied elements. * If a regular expression is passed, then field names matching the regular expression will be copied. * If a string/symbol is passed then it is name of the only copied field. * </td> * </tr> * <tr> * <td>`except`</td> * <td>`Array | Object | RegExp | String | Symbol`</td> * <td>`""` (empty string)</td> * <td> * Array, object, regular expression or string/symbol that defines names of fields/functions that shouldn't be copied. * <br> * If an object is passed then his fields with true values determine non-copied elements. * If a regular expression is passed, then field names matching the regular expression will not be copied. * If a string/symbol is passed then it is name of the only non-copied field. * </td> * </tr> * <tr> * <td>`filter`</td> * <td>`Function | RegExp`</td> * <td>`null`</td> * <td> * Function or regular expression that can be used to select elements that should be copied. * <br> * If regular expression is passed, only those fields will be copied whose values are matching regular expression. * <br> * If specified function returns `true` for a field, * the field will be copied in the target object. * <br> * An object with contextual data is passed into function (see details below). * </td> * </tr> * <tr> * <td>`otherName`</td> * <td>`Object`</td> * <td>`null`</td> * <td> * Defines "renaming table" for copied elements. * <br> * Fields of the table are names from a source object, values are the corresponding names in the target object. * <br> * For example, the call * <br> * <code> * mixing({}, {field: 1, func: "no-func"}, {otherName: {"field": "prop", "func": "method"}}) * </code> * <br> * will return the following object * <br> * `{prop: 1, method: "no-func"}` * </td> * </tr> * <tr> * <td>`change`</td> * <td>`Function | Object`</td> * <td>`null`</td> * <td> * Function or object that gives ability to change values that should be copied. * <br> * If an object is passed then his fields determine new values for copied elements. * <br> * If a function is passed then value returned by the function for a field will be copied into the target object * instead of original field's value. * <br> * An object with contextual data is passed into function (see details below). * </td> * </tr> * </table> * An object having the following fields is passed into `overwrite`, `filter` and `change` function: * <ul> * <li>`field` - field name * <li>`value` - field value from the source object * <li>`targetValue` - field value from the target object * <li>`target` - reference to the target object * <li>`source` - reference to the source object * </ul> * Default values of settings can be redefined by {@link module:mixing.setSettings setSettings} method. * <br> * `copy`, `except` and `filter` settings can be used together. * In such situation a field will be copied only when the field satisfies to all settings * (i.e. belongs to copied elements, not in exceptions and conforms to filter). * @return {Object} * Modified target object. * @alias module:mixing */ function mixing(destination, source, settings) { /*jshint boss:true, laxbreak:true*/ var destinationType = typeof destination; var sourceType = typeof source; if (destination && (destinationType === "object" || destinationType === "function") && source && (sourceType === "object" || sourceType === "function")) { var obj; // Prepare parameters if (typeof settings !== "object" || settings === null) { settings = defaultSettings || {}; } else if (defaultSettings) { obj = defaultSettings; defaultSettings = null; settings = mixing({}, [settings, obj]); defaultSettings = obj; } if (! Array.isArray(source) || settings.oneSource) { source = [source]; } // Prepare settings var getOwnPropertySymbols = Object.getOwnPropertySymbols, getPrototypeOf = Object.getPrototypeOf, options = { copyFunc: ("copyFunc" in settings ? settings.copyFunc : true), funcToProto: Boolean(settings.funcToProto), processSymbol: ("processSymbol" in settings ? settings.processSymbol : true) && typeof getOwnPropertySymbols === "function", mixFromArray: Boolean( ("mixFromArray" in settings) ? settings.mixFromArray : settings.mixArray ), mixToArray: Boolean( ("mixToArray" in settings) ? settings.mixToArray : settings.mixArray ), overwrite: settings.overwrite, ownProperty: Boolean(settings.ownProperty), recursive: Boolean(settings.recursive), otherNameMap: ("otherName" in settings ? settings.otherName : null), changeFunc: settings.changeFunc, copyMap: settings.copyMap, copyRegExp: settings.copyRegExp, exceptions: settings.exceptions, exceptRegExp: settings.exceptRegExp, filterRegExp: settings.filterRegExp }, bOwnProperty = options.ownProperty, bProcessSymbol = options.processSymbol, change = settings.change, copyList = settings.copy, exceptList = settings.except, filter = settings.filter, nI, nK, nL, nQ, propName; if (copyList) { copyList = prepareFieldList(copyList); options.copyMap = copyList.map; options.copyRegExp = copyList.regexp; } if (exceptList) { exceptList = prepareFieldList(exceptList, true); options.exceptions = exceptList.map; options.exceptRegExp = exceptList.regexp; } if (filter) { options[typeof filter === "object" ? "filterRegExp" : "filter"] = filter; } if (change) { options[typeof change === "function" ? "changeFunc" : "change"] = change; } // Copy fields and functions according to settings for (nI = 0, nL = source.length; nI < nL; nI++) { if (obj = source[nI]) { for (propName in obj) { copy(destination, obj, propName, options); } // Process symbol property keys if (bProcessSymbol) { exceptList = {}; do { copyList = getOwnPropertySymbols(obj); for (nK = 0, nQ = copyList.length; nK < nQ; nK++) { propName = copyList[nK]; if (! (propName in exceptList)) { copy(destination, obj, propName, options); exceptList[propName] = true; } } obj = bOwnProperty ? null : getPrototypeOf(obj); } while (obj); } } } } return destination; } /** * Copy values of all of the own properties from one or more source objects to the target object * (similar to `Object.assign`). * <br> * This function is a "wrap" for the following code: * <code><pre> * mixing(destination, Array.prototype.slice.call(arguments, 1), {overwrite: true, ownProperty: true}); * </pre></code> * * @param {Object | Function} destination * The target object into which fields and functions will be copied. * @param {...Object} source * An object whose contents will be copied. * If a source is a falsy value (e.g. `null` or `undefined`), the source will be skipped. * @return {Object} * Modified `target` object. */ mixing.assign = function(destination) { return mixing(destination, Array.prototype.slice.call(arguments, 1), {overwrite: true, ownProperty: true}); }; /** * Change values of fields of given object. * <br> * This function is a "wrap" for the following code: * <code><pre> * mixing(source, source, {change: change, overwrite: true, oneSource: true}); * </pre></code> * * @param {Array | Object} source * An array or an object whose fields should be modified. * @param {Function | Object} change * A function or an object that specifies the modification. See {@link module:mixing mixing} for details. * @return {Object} * Modified `source` object. */ mixing.change = function(source, change) { return mixing(source, source, {change: change, overwrite: true, oneSource: true}); }; /** * Make a copy of source object(s). * <br> * This function is a "wrap" for the following code: * <code><pre> * var copy = mixing({}, source, settings); * </pre></code> * * @param {Array | Object} source * Array of source objects or just one object whose contents will be copied. * @param {Object} [settings] * Operation settings. See {@link module:mixing mixing} for details. * @return {Object} * Newly created object containing contents of source objects. */ mixing.copy = function(source, settings) { return mixing({}, source, settings); }; /** * Copy fields from source object(s) into every object item of given array. * * @param {Array} destinationList * An array whose items should be modified. * @param {Array | Object} source * Array of source objects or just one object whose contents will be copied. * @param {Object} [settings] * Operation settings that will be applied to every copying. See {@link module:mixing mixing} for details. * @return {Array} * Original `destinationList` with possibly modified object items. */ mixing.mixToItems = function(destinationList, source, settings) { for (var nI = 0, nL = destinationList.length; nI < nL; nI++) { destinationList[nI] = mixing(destinationList[nI], source, settings); } return destinationList; }; /** * Make a copy of `this` object. * <br> * This function is a "wrap" for the following code: * <code><pre> * var copy = mixing({}, this, settings); * </pre></code> * It can be transferred to an object to use as a method. * * @param {Object} [settings] * Operation settings. See {@link module:mixing mixing} for details. * @return {Object} * Newly created object containing contents of `this` object. */ mixing.clone = function(settings) { return mixing({}, this, settings); }; /** * Filter `this` object. * <br> * This function is a "wrap" for the following code: * <code><pre> * var result = mixing({}, this, {filter: filter}); * </pre></code> * It can be transferred to an object to use as a method. * * @param {Function | Object} filter * Filter function to select fields or object that represents operation settings including filter function. * See {@link module:mixing mixing} for details. * @return {Object} * Newly created object containing fields of `this` object for which filter function returns true. */ mixing.filter = function(filter) { return mixing({}, this, typeof filter === "function" ? {filter: filter} : filter); }; /** * Copy and change values of fields of `this` object. * <br> * This function is a "wrap" for the following code: * <code><pre> * var result = mixing({}, this, {change: change}); * </pre></code> * It can be transferred to an object to use as a method. * * @param {Function | Object} change * Function to change values of copied fields or object that represents operation settings including change function. * See {@link module:mixing mixing} for details. * @return {Object} * Newly created object containing fields of `this` object with changed values. */ mixing.map = function(change) { return mixing({}, this, typeof change === "function" ? {change: change} : change); }; /** * Copy/add all fields and functions from source objects into `this` object. * As a result `this` object may be modified. * <br> * This function is a "wrap" for the following code: * <code><pre> * mixing(this, source, settings); * </pre></code> * It can be transferred to an object to use as a method. * * @param {Array | Object} source * Array of source objects or just one object whose contents will be copied. * @param {Object} [settings] * Operation settings. See {@link module:mixing mixing} for details. * @return {Object} * Modified `this` object. */ mixing.mix = function(source, settings) { return mixing(this, source, settings); }; /** * Change values of fields of `this` object. * <br> * This function is a "wrap" for the following code: * <code><pre> * mixing.change(this, change); * </pre></code> * It can be transferred to an object to use as a method. * * @param {Function | Object} change * A function or an object that specifies the modification. See {@link module:mixing mixing} for details. * @return {Object} * Modified `this` object. */ mixing.update = function(change) { return mixing.change(this, change); }; /** * Return default settings that were set earlier. * * @return {Object | undefined} * Default settings that were set earlier or `undefined / null` if default settings were not set. */ mixing.getSettings = function() { return defaultSettings; }; /** * Set (redefine, reset) default settings that should be used for subsequent {@link module:mixing mixing} calls. * * @param {Object | undefined} [settings] * Default settings that should be used for subsequent {@link module:mixing mixing} calls. * Initial default values will be used for settings that are not specified in the passed object. * Pass `undefined`, `null`, non-object or to call without parameter * to reset default settings to initial values. * @alias module:mixing.setSettings */ mixing.setSettings = function(settings) { defaultSettings = typeof settings === "object" ? settings : null; }; module.exports = mixing;