UNPKG

deepmerge-plus

Version:

用於深度(遞迴)合併 JavaScript 物件的函式庫 / A library for deep (recursive) merging of JavaScript objects

411 lines (406 loc) 16.5 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('is-mergeable-object')) : typeof define === 'function' && define.amd ? define(['exports', 'is-mergeable-object'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.DeepmergePlus = {}, global.isMergeableObject)); })(this, (function (exports, isMergeableObject) { 'use strict'; /** * 深度合併兩個 JavaScript 物件(可遞迴合併巢狀物件和陣列) * Deep (recursive) merge of two JavaScript objects * * @package deepmerge-plus * @version 3.0.2 */ /** * 建立空目標物件 / Create empty target object * * 根據輸入值類型回傳空陣列或空物件 * 用於深度合併時建立目標的空白副本 * * Returns empty array or object based on input value type * Used to create a blank copy of target during deep merge * * @param val - 輸入值 / Input value (array or object) * @returns 空陣列(若輸入為陣列)或空物件(若輸入為物件)/ Empty array (if input is array) or empty object (if input is object) * * @example * emptyTarget([1, 2, 3]) // 回傳 [] * emptyTarget({ a: 1 }) // 回傳 {} */ function emptyTarget(val) { return Array.isArray(val) ? [] : {}; } /** * 決定是否啟用深度複製功能 * 預設為 true(啟用),除非選項明確設為 false * * Determine whether to enable deep cloning * Default is true (enabled), unless options explicitly set to false * * @note 注意事項 / Note: * 即使 `options.clone` 設為 `false`,在某些情況下仍可能會產生 clone。 * 這是正常現象,因為深度合併需要確保輸出物件與輸入物件相互獨立, * 以避免意外修改原始資料。 * * Even if `options.clone` is set to `false`, cloning may still occur * in certain situations. This is expected behavior because deep merge * needs to ensure the output object is independent from the input objects * to prevent accidental modification of original data. */ function _shouldClone(optionsRuntime) { const clone = (optionsRuntime === null || optionsRuntime === void 0 ? void 0 : optionsRuntime.clone) !== false; return clone; } function _defaultCheckShouldNotUpsertValue(value, optionsRuntime, tmpRuntimeTarget, tmpRuntimeData) { var _tmpRuntimeTarget$tar; const targetValue = (_tmpRuntimeTarget$tar = tmpRuntimeTarget.target) === null || _tmpRuntimeTarget$tar === void 0 ? void 0 : _tmpRuntimeTarget$tar[tmpRuntimeTarget.key]; const shouldNotUpsertValue = !_isUndefined(targetValue); return shouldNotUpsertValue; } /** * 除非另有指定,否則複製值 / Clone value unless otherwise specified * * 決定是否需要深度複製輸入值: * 1. 檢查選項中的 clone 設定 * 2. 判斷值是否為可合併物件 * 3. 若需要合併則遞迴複製 * * Determines whether to deep clone the input value: * 1. Check clone option in optionsArgument * 2. Check if value is mergeable object * 3. Recursively clone if mergeable * * @param value - 要複製的值 / Value to clone * @param optionsRuntime - 合併選項 / Merge options * @param tmpRuntimeTarget - 內部快取資訊 / Internal cache information * @returns 複製後的值或原始值 / Cloned value or original value */ function cloneUnlessOtherwiseSpecified(value, optionsRuntime, tmpRuntimeTarget, tmpRuntimeData) { /** * 決定是否啟用深度複製功能 * 預設為 true(啟用),除非選項明確設為 false * * Determine whether to enable deep cloning * Default is true (enabled), unless options explicitly set to false * * @note 即使 `options.clone` 設為 `false`,在某些情況下仍可能會產生 clone * @note Even if `options.clone` is set to `false`, cloning may still occur */ const clone = _shouldClone(optionsRuntime); const bool = clone && _isMergeableObject(value, optionsRuntime, tmpRuntimeTarget, tmpRuntimeData); let ret = bool ? deepmerge(emptyTarget(value), value, optionsRuntime, tmpRuntimeData) : value; if (optionsRuntime !== null && optionsRuntime !== void 0 && optionsRuntime.keyValueOrMode && !bool && tmpRuntimeTarget && 'key' in tmpRuntimeTarget) { if (tmpRuntimeTarget.destination) { ret = tmpRuntimeTarget.destination[tmpRuntimeTarget.key] || ret; } if (tmpRuntimeTarget.target) { ret = tmpRuntimeTarget.target[tmpRuntimeTarget.key] || ret; } if (tmpRuntimeTarget.source) { ret = tmpRuntimeTarget.source[tmpRuntimeTarget.key] || ret; } } if (optionsRuntime !== null && optionsRuntime !== void 0 && optionsRuntime.keyValueUpsertMode && !bool && tmpRuntimeTarget && 'key' in tmpRuntimeTarget) { let shouldNotUpsertValue = false; if (typeof optionsRuntime.keyValueUpsertMode === 'function') { shouldNotUpsertValue = optionsRuntime.keyValueUpsertMode(value, optionsRuntime, tmpRuntimeTarget, tmpRuntimeData); } else if (optionsRuntime.keyValueUpsertMode === true) { var _tmpRuntimeTarget$tar2; const targetValue = (_tmpRuntimeTarget$tar2 = tmpRuntimeTarget.target) === null || _tmpRuntimeTarget$tar2 === void 0 ? void 0 : _tmpRuntimeTarget$tar2[tmpRuntimeTarget.key]; shouldNotUpsertValue = !_isUndefined(targetValue); } if (shouldNotUpsertValue) { let upsertValue; if (tmpRuntimeTarget.destination) { upsertValue !== null && upsertValue !== void 0 ? upsertValue : upsertValue = tmpRuntimeTarget.destination[tmpRuntimeTarget.key]; } if (tmpRuntimeTarget.target) { upsertValue !== null && upsertValue !== void 0 ? upsertValue : upsertValue = tmpRuntimeTarget.target[tmpRuntimeTarget.key]; } ret = upsertValue; } } return ret; } function _isUndefined(value) { return typeof value === 'undefined'; } function _isNull(value) { return value === null; } function _isNullOrUndefined(value) { return value === null || typeof value === 'undefined'; } /** * 檢查值是否為可合併物件 / Check if value is a mergeable object * * 判斷邏輯: * 1. 先檢查選項中的自訂 isMergeableObject 函式 * 2. 若無自訂函式,檢查 SYMBOL_IS_MERGEABLE 符號 * 3. 若無符號,使用預設的 isMergeableObject 函式 * * Decision logic: * 1. First check custom isMergeableObject function in options * 2. If no custom function, check SYMBOL_IS_MERGEABLE symbol * 3. If no symbol, use default isMergeableObject function * * @param value - 要檢查的值 / Value to check * @param optionsArgument - 合併選項 / Merge options * @param tmpRuntimeTarget - 內部快取資訊 / Internal cache information * @returns 是否可合併 / Whether mergeable */ function _isMergeableObject(value, optionsArgument, tmpRuntimeTarget, tmpRuntimeData) { var _optionsArgument$isMe; let ret = optionsArgument === null || optionsArgument === void 0 || (_optionsArgument$isMe = optionsArgument.isMergeableObject) === null || _optionsArgument$isMe === void 0 ? void 0 : _optionsArgument$isMe.call(optionsArgument, value, isMergeableObject, optionsArgument, tmpRuntimeTarget, tmpRuntimeData); if (ret === null || typeof ret === 'undefined') { if (typeof (value === null || value === void 0 ? void 0 : value[SYMBOL_IS_MERGEABLE]) == 'boolean') { ret = value[SYMBOL_IS_MERGEABLE]; } else { ret = isMergeableObject(value); } } return ret; } function defaultArrayMerge(target, source, optionsArgument, tmpRuntimeData) { // @ts-ignore tmpRuntimeData !== null && tmpRuntimeData !== void 0 ? tmpRuntimeData : tmpRuntimeData = _newTmpRuntimeData([]); const level = tmpRuntimeData.level + 1; const root = tmpRuntimeData.root; const parent = []; tmpRuntimeData.parent[tmpRuntimeData.key] = parent; return target.concat(source).reduce((parent, element, index) => { element = cloneUnlessOtherwiseSpecified(element, optionsArgument, { key: index }, { level, paths: [...tmpRuntimeData.paths, index], root, parent, key: index }); parent[index] = element; return parent; }, parent); } /** * 合併兩個物件 / Merge two objects * * 這是 deepmerge 處理物件合併的核心函式 * 負責將來源物件的屬性合併到目標物件 * * This is the core function for object merging in deepmerge * Responsible for merging source object properties into target object * * 由左至右的概念 / Left-to-right concept: * merge(target, source, options) 中: * - 第一個參數 target 是「左側」(left)- 被合併的物件,代表現有值 * - 第二個參數 source 是「右側」(right)- 要合併進來的物件,代表新值 * - 合併結果是將「右側」的值合併進「左側」 * * In merge(target, source, options): * - First parameter target is "left" - the object being merged INTO, represents existing values * - Second parameter source is "right" - the object being merged FROM, represents new values * - The result merges "right" values INTO "left" * * 處理邏輯 / Processing logic: * 1. 建立目的物件(destination)用於存放合併結果 * 2. 遍歷目標物件的所有鍵值: * - 複製到目的物件,進行深度複製 * 3. 遍歷來源物件的所有鍵值: * - 若鍵不存在於目標 OR 來源值不可合併 → 直接複製 * - 若鍵存在於目標 AND 來源值可合併 → 遞迴呼叫 deepmerge 進行深度合併 * * @param target - 目標物件(被合併的物件)/ Target object (the object being merged into) * @param source - 來源物件(要合併的物件)/ Source object (the object to merge from) * @param optionsArgument - 合併選項 / Merge options * @returns 合併後的新物件 / New merged object */ function mergeObject(target, source, optionsArgument, tmpRuntimeData) { let destination = {}; if (!tmpRuntimeData) { tmpRuntimeData = _newTmpRuntimeData(destination); } if (_isMergeableObject(target, optionsArgument, void 0, tmpRuntimeData)) { Object.keys(target).forEach(function (key) { destination[key] = cloneUnlessOtherwiseSpecified(target[key], optionsArgument, { key, source, target, destination }, { level: tmpRuntimeData.level + 1, paths: [...tmpRuntimeData.paths, key], root: tmpRuntimeData.root, parent: destination, key: key }); }); } Object.keys(source).forEach(function (key) { if (!_isMergeableObject(source[key], optionsArgument, { key, source, target }, tmpRuntimeData) || !target[key]) { destination[key] = cloneUnlessOtherwiseSpecified(source[key], optionsArgument, { key, source, target }, { level: tmpRuntimeData.level + 1, paths: [...tmpRuntimeData.paths, key], root: tmpRuntimeData.root, parent: destination, key: key }); } else { destination[key] = deepmerge(target[key], source[key], optionsArgument, { level: tmpRuntimeData.level + 1, paths: [...tmpRuntimeData.paths, key], root: tmpRuntimeData.root, parent: destination, key: key }); } }); return destination; } function _handleOptions(optionsArgument) { const options = optionsArgument || {}; // @ts-ignore return options; } function deepmerge(target, source, optionsArgument, tmpRuntimeData) { const sourceIsArray = Array.isArray(source); const targetIsArray = Array.isArray(target); const options = _handleOptions(optionsArgument); const sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; if (!sourceAndTargetTypesMatch) { return cloneUnlessOtherwiseSpecified(source, optionsArgument, { target, source }, tmpRuntimeData); } else if (sourceIsArray) { var _options$arrayMerge; return ((_options$arrayMerge = options === null || options === void 0 ? void 0 : options.arrayMerge) !== null && _options$arrayMerge !== void 0 ? _options$arrayMerge : defaultArrayMerge)(target, source, optionsArgument, tmpRuntimeData); } else { return mergeObject(target, source, optionsArgument, tmpRuntimeData); } } /** * 檢查值是否為可合併物件 / Check if value is a mergeable object * * 這是一個便捷函式,包裝了 is-mergeable-object 模組 * 用於快速判斷給定的值是否可以被 deepmerge 合併 * * This is a convenience function that wraps the is-mergeable-object module * Used to quickly determine if a given value can be merged by deepmerge * * 可合併的類型通常包括: * - 普通物件(plain objects) * - 陣列(arrays) * - 不可合併的類型(如原始類型、函式、Date 等)會回傳 false * * Mergeable types typically include: * - Plain objects * - Arrays * - Non-mergeable types (such as primitives, functions, Date, etc.) return false * * @param value - 要檢查的值 / Value to check * @returns 是否可合併(true 表示可以進行深度合併)/ Whether mergeable (true means deep merge can be performed) * * @example * isMergeable({}) // 回傳 / returns true * isMergeable([]) // 回傳 / returns true * isMergeable('string') // 回傳 / returns false * isMergeable(123) // 回傳 / returns false * isMergeable(new Date()) // 回傳 / returns false */ function isMergeable(value) { return isMergeableObject(value); } const SYMBOL_IS_MERGEABLE = /*#__PURE__*/Symbol.for('SYMBOL_IS_MERGEABLE'); /** * 建立新的執行時資料 / Create new runtime data * * 在開始新的合併操作時呼叫此函式建立初始的 tmpRuntimeData * 初始狀態為根層級,level 為 0,paths 為空陣列 * * Called when starting a new merge operation to create initial tmpRuntimeData * Initial state is root level, level is 0, paths is empty array * * @template R - root 物件的類型 * @template T - parent 物件的類型(預設與 R 相同) * @param root - 初始的 destination 物件(將作為 root 傳遞) * @returns 初始化的 tmpRuntimeData * * @example * const data = _newTmpRuntimeData({}); * // data.level === 0 * // data.paths === [] * // data.root === {} * // data.parent === {} * // data.key === undefined */ function _newTmpRuntimeData(root) { const tmpRuntimeData = { level: 0, paths: [], root: root, parent: root, key: undefined }; return tmpRuntimeData; } function deepmergeAll(array, optionsArgument) { if (!Array.isArray(array)) { throw new Error('first argument should be an array'); } // @ts-ignore return array.reduce(function (prev, next) { return deepmerge(prev, next, optionsArgument); }, {}); } // @ts-ignore { Object.defineProperty(deepmerge, "__esModule", { value: true }); Object.defineProperty(deepmerge, 'deepmerge', { value: deepmerge }); Object.defineProperty(deepmerge, 'default', { value: deepmerge }); Object.defineProperty(deepmerge, 'isMergeable', { value: isMergeable }); Object.defineProperty(deepmerge, 'SYMBOL_IS_MERGEABLE', { value: SYMBOL_IS_MERGEABLE }); Object.defineProperty(deepmerge, 'deepmergeAll', { value: deepmergeAll }); Object.defineProperty(deepmerge, 'all', { value: deepmergeAll }); Object.defineProperty(deepmerge, '_isMergeableObject', { value: _isMergeableObject }); } exports.SYMBOL_IS_MERGEABLE = SYMBOL_IS_MERGEABLE; exports._defaultCheckShouldNotUpsertValue = _defaultCheckShouldNotUpsertValue; exports._handleOptions = _handleOptions; exports._isMergeableObject = _isMergeableObject; exports._isNull = _isNull; exports._isNullOrUndefined = _isNullOrUndefined; exports._isUndefined = _isUndefined; exports._newTmpRuntimeData = _newTmpRuntimeData; exports._shouldClone = _shouldClone; exports.all = deepmergeAll; exports.deepmerge = deepmerge; exports.deepmergeAll = deepmergeAll; exports.default = deepmerge; exports.isMergeable = isMergeable; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=index.umd.development.cjs.map