UNPKG

deepmerge-plus

Version:

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

1,068 lines (998 loc) 39.3 kB
/** * 深度合併兩個 JavaScript 物件(可遞迴合併巢狀物件和陣列) * Deep (recursive) merge of two JavaScript objects * * @package deepmerge-plus * @version 3.0.2 */ import isMergeableObject from 'is-mergeable-object'; import { ITSPickExtra, ITSWriteable, ITSToWriteableArray, ITSPropertyKey } from 'ts-type'; /** * 建立空目標物件 / 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. */ export function _shouldClone(optionsRuntime?: IOptions) { // do not change or remove this code logic // const clone = !optionsRuntime || optionsRuntime.clone !== false; const clone = optionsRuntime?.clone !== false; return clone; } /** * 檢查目標物件中該 key 的值是否為 undefined * Check if target key value is undefined */ export function _defaultCheckShouldNotUpsertValue(value, optionsRuntime: IOptions, tmpRuntimeTarget: ICache, tmpRuntimeData: ITmpRuntimeData) { const targetValue = tmpRuntimeTarget.target?.[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: IOptions, tmpRuntimeTarget: ICache, tmpRuntimeData: ITmpRuntimeData) { /** * 決定是否啟用深度複製功能 * 預設為 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); /** * 判斷當前值是否需要進行深度複製 * 條件:clone 為 true 且值為可合併物件 * Determine if current value needs deep cloning * Condition: clone is true AND value is a mergeable object */ const bool = clone && _isMergeableObject(value, optionsRuntime, tmpRuntimeTarget, tmpRuntimeData); /** * 根據 bool 條件決定回傳值 * 若需要複製:建立空目標並遞迴合併(深度複製) * 若不需要複製:直接回傳原始值 * Return value based on bool condition * If needs cloning: create empty target and recursively merge (deep clone) * If not needs cloning: return original value directly */ let ret = (bool) ? deepmerge(emptyTarget(value), value, optionsRuntime, tmpRuntimeData) : value; /** * keyValueOrMode 模式處理 * 當啟用此模式且值不需要合併時,優先使用目標中已存在的值 * 檢查順序:destination > target > source * keyValueOrMode mode handling * When enabled and value doesn't need merging, prefer existing values in target * * 由左至右的概念 / Left-to-right concept: * merge(target, source, options) 中: * - target 是「左側」(left)- 被合併的物件,代表現有值 * - source 是「右側」(right)- 要合併進來的物件,代表新值 * - 當 source 值為 falsy 時,使用「左側」的值作為回退 * * In merge(target, source, options): * - target is "left" - the object being merged INTO, represents existing values * - source is "right" - the object being merged FROM, represents new values * - When source value is falsy, use "left" value as fallback * * Check order: destination > target > source */ if (optionsRuntime?.keyValueOrMode && !bool && tmpRuntimeTarget && ('key' in tmpRuntimeTarget)) { /** 檢查目的物件中是否有現有值 / Check if destination has existing value */ if (tmpRuntimeTarget.destination) { //console.log('destination', tmpRuntimeTarget.destination[tmpRuntimeTarget.key], ret, tmpRuntimeTarget.key); ret = tmpRuntimeTarget.destination[tmpRuntimeTarget.key] || ret; } /** 檢查目標物件中是否有現有值 / Check if target has existing value */ if (tmpRuntimeTarget.target) { //console.log('target', tmpRuntimeTarget.target[tmpRuntimeTarget.key], ret, tmpRuntimeTarget.key); ret = tmpRuntimeTarget.target[tmpRuntimeTarget.key] || ret; } /** 檢查來源物件中是否有現有值 / Check if source has existing value */ if (tmpRuntimeTarget.source) { //console.log('source', tmpRuntimeTarget.source[tmpRuntimeTarget.key], ret, tmpRuntimeTarget.key); ret = tmpRuntimeTarget.source[tmpRuntimeTarget.key] || ret; } } // console.dir({ // value, // ret, // optionsRuntime, // tmpRuntimeTarget, // tmpRuntimeData, // targetValue: tmpRuntimeTarget.target?.[tmpRuntimeTarget.key], // bool, // }, { depth: 2 }); /** * keyValueUpsertMode 模式處理 * 當啟用此模式時,根據條件決定是否應該使用目標中的現有值 * keyValueUpsertMode mode handling * When enabled, decides whether to use existing value in target based on condition * * 由左至右的概念 / Left-to-right concept: * merge(target, source, options) 中: * - target 是「左側」(left)- 被合併的物件,代表現有值 * - source 是「右側」(right)- 要合併進來的物件,代表新值 * - 合併結果會傾向保留「左側」的值(除非條件允許使用「右側」的值) * * In merge(target, source, options): * - target is "left" - the object being merged INTO, represents existing values * - source is "right" - the object being merged FROM, represents new values * - The merged result tends to preserve "left" values (unless conditions allow using "right" values) * * 為 true 時:只會在目標 key 的值為 undefined 時才使用目標的值 * 為 function 時:只會在函式回傳 true 時使用目標的值 * * When true: Only use target's value when target key value is undefined * When function: Only use target's value when function returns true */ if (optionsRuntime?.keyValueUpsertMode && !bool && tmpRuntimeTarget && ('key' in tmpRuntimeTarget)) { /** 檢查是否應該使用目標的值 / Check if should use target's value */ let shouldNotUpsertValue = false; if (typeof optionsRuntime.keyValueUpsertMode === 'function') { /** 使用自訂函式判斷 / Use custom function to determine */ shouldNotUpsertValue = optionsRuntime.keyValueUpsertMode(value, optionsRuntime, tmpRuntimeTarget, tmpRuntimeData); } else if (optionsRuntime.keyValueUpsertMode === true) { /** 檢查目標物件中該 key 的值是否為 undefined / Check if target key value is undefined */ const targetValue = tmpRuntimeTarget.target?.[tmpRuntimeTarget.key]; shouldNotUpsertValue = !_isUndefined(targetValue); } /** 若符合條件,則使用目標中的現有值 / If condition met, use existing value in target */ if (shouldNotUpsertValue) { let upsertValue: any; /** 檢查目的物件中是否有現有值 / Check if destination has existing value */ if (tmpRuntimeTarget.destination) { upsertValue ??= tmpRuntimeTarget.destination[tmpRuntimeTarget.key]; } /** 檢查目標物件中是否有現有值 / Check if target has existing value */ if (tmpRuntimeTarget.target) { upsertValue ??= tmpRuntimeTarget.target[tmpRuntimeTarget.key]; } ret = upsertValue; } // console.dir({ // shouldNotUpsertValue, // key: tmpRuntimeTarget.key, // ret, // value, // }); } return ret; } export function _isUndefined(value: unknown): value is undefined { return typeof value === 'undefined' } export function _isNull(value: unknown): value is null { return value === null } export function _isNullOrUndefined(value: unknown): value is null { 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 */ export function _isMergeableObject(value, optionsArgument: IOptions, tmpRuntimeTarget: ICache, tmpRuntimeData: ITmpRuntimeData): boolean { /** * 步驟 1:嘗試使用自訂 isMergeableObject 函式(若存在) * Step 1: Try using custom isMergeableObject function (if exists) */ let ret = optionsArgument?.isMergeableObject?.(value, isMergeableObject, optionsArgument, tmpRuntimeTarget, tmpRuntimeData) as any; /** * 步驟 2:若自訂函式未回傳明確結果(null 或 undefined) * 檢查 SYMBOL_IS_MERGEABLE 符號 * Step 2: If custom function doesn't return definite result (null or undefined) * Check SYMBOL_IS_MERGEABLE symbol */ if (ret === null || typeof ret === 'undefined') { /** 檢查值是否帶有可合併標記符號 / Check if value has mergeable marker symbol */ if ((typeof value?.[SYMBOL_IS_MERGEABLE] == 'boolean')) { ret = value[SYMBOL_IS_MERGEABLE]; } /** 若無符號,使用預設的 isMergeableObject 函式 / If no symbol, use default isMergeableObject function */ else { ret = isMergeableObject(value); } } return ret } /** * 預設陣列合併函式 / Default array merge function * * 將來源陣列串接到目標陣列,並對每個元素進行深度複製 * 這是 deepmerge 的預設陣列合併策略 * * Concatenates source array to target array and deep clones each element * This is the default array merge strategy for deepmerge * * 處理邏輯 / Processing logic: * 1. 將來源陣列串接到目標陣列末尾 * 2. 對每個元素進行條件性深度複製 * 3. 確保合併後的陣列元素是獨立的副本 * * @param target - 目標陣列(被合併的陣列)/ Target array (the array being merged into) * @param source - 來源陣列(要合併的陣列)/ Source array (the array to merge from) * @param optionsArgument - 合併選項 / Merge options * @returns 合併後的新陣列 / New merged array */ function defaultArrayMerge<T extends any[]>(target: T, source: any[], optionsArgument?: IOptions, tmpRuntimeData?: ITmpRuntimeData<T>): T[]; function defaultArrayMerge(target: any[], source: any[], optionsArgument?: IOptions, tmpRuntimeData?: ITmpRuntimeData): any[] function defaultArrayMerge(target: any[], source: any[], optionsArgument?: IOptions, tmpRuntimeData?: ITmpRuntimeData): any[] { // @ts-ignore tmpRuntimeData ??= _newTmpRuntimeData<any[]>([]); const level = tmpRuntimeData.level + 1; const root = tmpRuntimeData.root; const parent = [] as any[]; (tmpRuntimeData.parent as any)[tmpRuntimeData.key] = parent; /** * 1. 串接目標陣列和來源陣列 * 2. 對每個元素進行深度複製(確保元素是獨立的副本) * 1. Concatenate target array and source array * 2. Deep clone each element (ensuring elements are independent copies) */ return target.concat(source as any).reduce((parent, element, index) => { /** * 對每個元素進行條件性複製 * 若為可合併物件則深度複製,否則直接複製 * Conditionally clone each element * Deep clone if mergeable, otherwise copy directly */ 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: IOptions, tmpRuntimeData: ITmpRuntimeData) { let destination = {}; if (!tmpRuntimeData) { tmpRuntimeData = _newTmpRuntimeData(destination); } /** * 處理階段 1:遍歷目標物件的所有鍵值 * 將目標物件的屬性複製到目的物件(進行深度複製) * Processing phase 1: Iterate all keys of target object * Copy target object properties to destination (deep clone) */ 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 }) }) } /** * 處理階段 2:遍歷來源物件的所有鍵值 * 根據是否存在於目標中以及是否可合併來決定合併策略 * Processing phase 2: Iterate all keys of source object * Decide merge strategy based on whether key exists in target and if mergeable */ Object.keys(source).forEach(function (key) { /** * 判斷邏輯: * - 若來源值不可合併 OR 目標中沒有此鍵 → 直接複製 * - 若來源值可合併且目標中也有此鍵 → 遞迴合併 * Decision logic: * - If source value not mergeable OR key doesn't exist in target → copy directly * - If source value mergeable AND key exists in target → recursively merge */ 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 } /** * 深度合併結果類型 / Deep merge result type * * 合併兩個物件後的類型,推斷規則如下: * 1. 取得 T2 中不在 T1 的鍵(新增的鍵) * 2. 與 T1 的鍵進行交集(保留 T1 的鍵) * 3. 確保結果為可寫入類型 * * Type after merging two objects, inference rules: * 1. Get keys in T2 that are not in T1 (new keys) * 2. Intersect with keys in T1 (preserve T1's keys) * 3. Ensure result is writeable type * * @template T1 - 目標物件類型 / Target object type * @template T2 - 來源物件類型 / Source object type */ export type IDeepmergeResult<T1, T2> = ITSWriteable<ITSPickExtra<T2, Exclude<keyof T2, keyof T1>> & T1>; /** * 初始化選項 * 若未提供則使用預設設定(使用預設陣列合併函式) * * Initialize options * Use default settings if not provided (use default array merge function) */ export function _handleOptions(optionsArgument?: IOptions): IOptions { const options: IOptions = optionsArgument || {}; // @ts-ignore // options.arrayMerge ??= defaultArrayMerge; return options; } /** * 深度合併函式 / Deep merge function * * 主要的合併函式,處理物件和陣列的深度合併 * Main merge function that handles deep merging of objects and arrays * * 處理邏輯 / Processing logic: * 1. 檢查來源和目標是否為陣列 * 2. 檢查類型是否匹配(兩者都必須是陣列或都不是) * 3. 若類型不匹配,返回來源物件的克隆 * 4. 若兩者都是陣列,使用 arrayMerge 函式合併 * 5. 否則,使用 mergeObject 進行物件合併 * * @param T1 - 目標物件類型 / Target object type * @param T2 - 來源物件類型 / Source object type * @param target - 目標物件 / Target object * @param source - 來源物件 / Source object * @param optionsArgument - 合併選項 / Merge options * @returns 合併後的物件 / Merged object */ export function deepmerge<T1 extends unknown[], T2 extends unknown[]>(x: T1, y: T2, options?: IOptions, tmpRuntimeData?: ITmpRuntimeData): [...T1, ...T2] export function deepmerge<T1, T2>(x: T1[], y: T2[], options?: IOptions, tmpRuntimeData?: ITmpRuntimeData): (T2 | T1)[] export function deepmerge<T1, T2>(x: T1, y: T2, options?: IOptions, tmpRuntimeData?: ITmpRuntimeData): IDeepmergeResult<T1, T2> export function deepmerge<T>(x: Partial<NoInfer<T>>, y: Partial<NoInfer<T>>, options?: IOptions, tmpRuntimeData?: ITmpRuntimeData): ITSWriteable<T> export function deepmerge(target, source, optionsArgument, tmpRuntimeData: ITmpRuntimeData) { /** * 步驟 1:類型檢測 * 判斷來源和目標是否為陣列 * * Step 1: Type detection * Check if source and target are arrays */ const sourceIsArray = Array.isArray(source); const targetIsArray = Array.isArray(target); /** * 步驟 2:初始化選項 * 若未提供則使用預設設定(使用預設陣列合併函式) * * Step 2: Initialize options * Use default settings if not provided (use default array merge function) */ const options = _handleOptions(optionsArgument); /** * 步驟 3:類型匹配檢查 * 確保兩者都是陣列或都不是陣列 * * Step 3: Type matching check * Ensure both are arrays or both are not arrays */ const sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; /** * 情境 1:類型不匹配(陣列 vs 物件) * 回傳來源物件的克隆,不進行合併 * * Scenario 1: Type mismatch (array vs object) * Return clone of source object, no merging * * 範例 / Example: * deepmerge([], {}) // 回傳 {} 的克隆 / returns clone of {} * deepmerge({}, []) // 回傳 [] 的克隆 / returns clone of [] */ if (!sourceAndTargetTypesMatch) { return cloneUnlessOtherwiseSpecified(source, optionsArgument, { target, source, }, tmpRuntimeData); } /** * 情境 2:兩者都是陣列 * 使用 arrayMerge 函式進行合併(預設為串接) * * Scenario 2: Both are arrays * Use arrayMerge function to merge (default is concatenation) * * 範例 / Example: * deepmerge([1, 2], [3, 4]) * // 回傳 / returns [1, 2, 3, 4](預設行為)/ (default behavior) */ else if (sourceIsArray) { return (options?.arrayMerge ?? defaultArrayMerge)(target, source, optionsArgument, tmpRuntimeData); } /** * 情境 3:兩者都是物件 * 使用 mergeObject 進行物件屬性合併 * * Scenario 3: Both are objects * Use mergeObject to merge object properties * * 範例 / Example: * deepmerge({ a: 1 }, { b: 2 }) * // 回傳 / returns { a: 1, b: 2 } */ else { return mergeObject(target, source, optionsArgument, tmpRuntimeData); } } /** * 內部快取介面 / Internal cache interface * * 用於在合併過程中傳遞上下文資訊 * 這些資訊在遞迴合併時用於維持物件間的引用關係 * * 與 ITmpRuntimeData 的差異: * - ICache: 追蹤 source、target、destination 的引用關係 * - ITmpRuntimeData: 追蹤遞迴路徑和深度資訊 * * Used to pass context information during merge operations * This information is used during recursive merge to maintain object references * * Difference from ITmpRuntimeData: * - ICache: Tracks source, target, destination reference relationships * - ITmpRuntimeData: Tracks recursion path and depth information * * @example * { * key: 'user', // 目前處理的鍵名 * source: { ... }, // 來源物件 * target: { ... }, // 目標物件 * destination: { ... } // 目的物件(目前合併結果) * } * * `tmpRuntimeTarget: ICache` */ export interface ICache { /** 鍵名 / Key name */ key? /** 來源物件 / Source object */ source? /** 目標物件 / Target object */ target? /** 目的物件 / Destination object */ destination? } export type IAnyRecord = Record<ITSPropertyKey | number, any>; /** * 執行時資料介面 / Runtime data interface * * 用於在 deepmerge 遞迴呼叫過程中傳遞上下文資訊 * 追蹤目前在合併樹中的位置和路徑 * * Used to pass context information during deepmerge recursive calls * Tracks current position and path in the merge tree * * 屬性說明 / Properties Description: * - level: 目前的遞迴深度(根為 0,每遞迴一次 +1) * - paths: 從根到目前位置的路徑陣列(可用於 _.get(root, paths)) * - root: 最頂層的 destination 物件(永遠不變) * - parent: 目前層級的父物件 * - key: 目前正在處理的鍵名 * * 遞迴流程範例 / Recursive Flow Example: * 合併 { a: { b: 1 } } 和 { a: { c: 2 } } 時: * - 根層級: level=0, paths=[], root={}, parent={}, key=undefined * - level1: level=1, paths=['a'], root={}, parent={}, key='a' * - level2: level=2, paths=['a','b'], root={}, parent={a:{}}, key='b' * * @template T - parent 物件的類型(通常與 root 相同或為其子物件) * @template R - root 物件的類型 */ export interface ITmpRuntimeData<T extends IAnyRecord = IAnyRecord, R extends IAnyRecord = IAnyRecord> { /** 遞迴深度計數(根為 0)/ Recursion depth count (root is 0) */ readonly level: number; /** 從根到目前位置的路徑陣列 / Path array from root to current position */ readonly paths: (keyof R | keyof T | ITSPropertyKey | number)[]; /** 最頂層的 destination 物件(永遠不變)/ Top-level destination object (always unchanged) */ readonly root: R; /** 目前層級的父物件 / Parent object at current level */ readonly parent: T; /** 目前正在處理的鍵名 / Key currently being processed */ readonly key: keyof T; } /** * 合併選項介面 / Merge options interface * * 定義 deepmerge 函式的可選配置參數 * Allows customization of merge behavior * * Defines optional configuration parameters for deepmerge function */ export interface IOptions { /** * 決定是否啟用深度複製功能 * 預設為 true(啟用),除非選項明確設為 false * * Determine whether to enable deep cloning * Default is true (enabled), unless options explicitly set to false * * @note 注意事項 / Note: * - 即使設為 `false`,在部分狀況下仍可能會忽略此設定並產生 clone * - 此行為為正常現象,因為深度合併需要確保輸出物件與輸入物件相互獨立 * * - Even if 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 * * @default true */ clone?: boolean; /** * 自訂陣列合併函式 * Custom array merge function * * 允許自訂陣列的合併行為 * 預設行為是串接兩個陣列 * * Allows customization of array merge behavior * Default behavior is to concatenate two arrays * * @param destination - 目標陣列 / Destination array * @param source - 來源陣列 / Source array * @param options - 合併選項 / Merge options * @returns 合併後的陣列 / Merged array */ arrayMerge?<T extends any[]>(target: T, source: any[], options?: IOptions, tmpRuntimeData?: ITmpRuntimeData<T>): T[]; arrayMerge?(target: any[], source: any[], options?: IOptions, tmpRuntimeData?: ITmpRuntimeData): any[]; /** * 自訂可合併物件判斷函式 * Custom mergeable object check function * * 允許自訂哪些物件可以被合併 * 可用於支援自訂類型(如 Map、Set 等) * * Allows customizing which objects can be merged * Can be used to support custom types (like Map, Set, etc.) * * @param value - 要檢查的值 / Value to check * @param isMergeableObject - 預設的判斷函式 / Default check function * @param optionsArgument - 合併選項 / Merge options * @param tmpRuntimeData - 執行時資料 / Runtime data */ isMergeableObject?(value, isMergeableObject: typeof isMergeable, optionsArgument?: IOptions, tmpRuntimeTarget?: ICache, tmpRuntimeData?: ITmpRuntimeData): void; isMergeableObject?(value, isMergeableObject: typeof isMergeable, optionsArgument?: IOptions, tmpRuntimeTarget?: ICache, tmpRuntimeData?: ITmpRuntimeData): boolean; /** * (val = old || new) 模式 * (val = old || new) mode * * 啟用時會保留目標中已存在的值 * 當來源物件和目標物件都有相同鍵時,優先使用目標的值 * * When enabled, preserves values that already exist in target * When both source and target have the same key, priority is given to target's value * * @deprecated 棄用,建議使用 `keyValueUpsertMode` 取代 * @deprecated Deprecated, use `keyValueUpsertMode` instead * * @note 注意事項 / Note: * 此選項保留為相容性用途,不建議在新程式碼中使用。 * 若有需要自訂合併邏輯,建議使用 `keyValueUpsertMode` 選項。 * * This option is kept for backward compatibility and is not recommended for new code. * For custom merge logic, use the `keyValueUpsertMode` option instead. * * @example * const target = { name: 'Alice', age: 25 }; * const source = { name: 'Bob', city: 'Taipei' }; * // keyValueOrMode: true 時 / when keyValueOrMode: true * // 結果: { name: 'Alice', age: 25, city: 'Taipei' } * // name 保留目標的值,age 來自目標,city 來自來源 */ keyValueOrMode?: boolean, /** * Upsert 模式(選擇性更新) * Upsert mode (selective update) * * 控制何時應該更新目標中的值 * 為 true 時:保留原有值(不取代),只在目標 key 的值為 undefined 時才使用來源的值 * 為 function 時:只在函式回傳 true 時保留原有值(不取代) * * Control when to update values in target * When true: Preserve existing values (don't replace), only use source's value when target key value is undefined * When function: Only preserve existing values (don't replace) when function returns true * * @note 重要說明 / Important Note: * 當 keyValueUpsertMode 為 true 時,表示「不取代」模式: * - 如果目標值不是 undefined,則保留目標的原始值 * - 只有當目標值是 undefined 時,才會使用來源的值 * 這包括 null、0、false、空字串等 falsy 值,都會被保留 * * When keyValueUpsertMode is true, it means "don't replace" mode: * - If target value is not undefined, preserve the original target value * - Only use source's value when target value is undefined * This includes falsy values like null, 0, false, empty string, which are all preserved * * @note 升級說明 / Upgrade Note: * 此選項為 `keyValueOrMode` 的升級版本,解決了以下問題: * - 使用 `??` 取代 `||` 運算子,避免 falsy 值(如 `0`、`''`、`false`)被錯誤處理 * - 支援自訂函式,可根據條件靈活決定是否保留目標的值 * - 行為更直觀,true 時明確表示「保留原有值」 * * This option is an upgraded version of `keyValueOrMode`, solving the following issues: * - Uses `??` instead of `||` operator to avoid incorrect handling of falsy values (like `0`, `''`, `false`) * - Supports custom functions for flexible conditional logic * - More intuitive behavior, true clearly means "preserve existing values" * * @example * const target = { name: 'Alice', age: undefined, count: 0, email: null }; * const source = { name: 'Bob', age: 30, count: 5, email: 'bob@example.com' }; * // keyValueUpsertMode: true 時 / when keyValueUpsertMode: true * // 結果: { name: 'Alice', age: 30, count: 0, email: null } * // name: 保留目標值 'Alice'(不是 undefined) * // age: 使用來源值 30(目標值是 undefined) * // count: 保留目標值 0(不是 undefined) * // email: 保留目標值 null(不是 undefined) * * @example * // 使用自訂函式 / Using custom function * keyValueUpsertMode: (value, options, tmpRuntimeTarget, tmpRuntimeData) => { * return tmpRuntimeTarget.target?.[tmpRuntimeTarget.key] !== undefined; * } */ keyValueUpsertMode?: | boolean | ((value: unknown, optionsRuntime?: IOptions, tmpRuntimeTarget?: ICache, tmpRuntimeData?: ITmpRuntimeData) => boolean); } /** * 檢查值是否為可合併物件 / 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 */ export function isMergeable(value: any): boolean { return isMergeableObject(value) } /** * 可合併物件標記符號 / Mergeable object marker symbol * * 全域符號,用於在物件上標記是否為可合併物件 * 這是一個全域可共享的 Symbol,可跨模組使用 * * Global symbol used to mark whether an object is mergeable * This is a globally shareable Symbol that can be used across modules * * 用法 / Usage: * ```typescript * const obj = { name: 'test' }; * obj[SYMBOL_IS_MERGEABLE] = true; // 標記為可合併 / Mark as mergeable * obj[SYMBOL_IS_MERGEABLE] = false; // 標記為不可合併 / Mark as not mergeable * ``` * * 優勢 / Advantages: * - 可以自訂物件的可合併性 * - 可以自訂物件的可合併性 * - 避免與物件原有屬性衝突 * - Avoid conflicts with object's original properties */ const SYMBOL_IS_MERGEABLE = Symbol.for('SYMBOL_IS_MERGEABLE'); export { 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 */ export function _newTmpRuntimeData<R extends ITmpRuntimeData["root"], T extends IAnyRecord = R>(root: R) { const tmpRuntimeData = { level: 0, paths: [], root: root, parent: root, key: undefined, } return tmpRuntimeData as any as ITmpRuntimeData<T, R>; } /** * 合併多個物件 / Merge multiple objects * * 將陣列中的所有物件依序合併成單一物件 * 這是 deepmerge 的陣列版本,適用於合併多個物件 * * Merges all objects in the array into a single object sequentially * This is the array version of deepmerge, suitable for merging multiple objects * * 處理邏輯 / Processing logic: * 1. 驗證輸入參數是否為陣列 * 2. 使用 reduce 依序合併所有物件 * 3. 初始值為空物件,每次迭代都會與下一個物件合併 * * @param array - 要合併的物件陣列 / Array of objects to merge * @param optionsArgument - 合併選項 / Merge options * @returns 合併後的物件 / Merged object * @throws 若第一個參數不是陣列,則拋出錯誤 / Throws error if first argument is not an array * * @example * deepmergeAll([{ a: 1 }, { b: 2 }, { c: 3 }]) * // 回傳 / returns { a: 1, b: 2, c: 3 } */ export function deepmergeAll<T1, T2 = any>(array: [T1, ...T2[]], optionsArgument?: IOptions): IDeepmergeResult<T1, T2> export function deepmergeAll<T1>(array: T1[], optionsArgument?: IOptions): T1 export function deepmergeAll<T>(array: any[], optionsArgument?: IOptions): T { /** * 驗證輸入參數是否為陣列 * 若不是陣列則拋出錯誤 * Validate input parameter is an array * Throw error if not an array */ if (!Array.isArray(array)) { throw new Error('first argument should be an array') } /** * 使用 reduce 依序合併所有物件 * 初始值為空物件,每次迭代都會與下一個物件合併 * Use reduce to sequentially merge all objects * Initial value is empty object, each iteration merges with next object */ // @ts-ignore return array.reduce(function (prev, next) { return deepmerge(prev, next, optionsArgument) }, {}) } export { deepmergeAll as all } export default deepmerge /** * CommonJS 環境兼容性設定 * CommonJS environment compatibility settings * * 為非 ESM 環境添加必要的屬性: * - __esModule: 標記為 ES 模組 * - deepmerge: 指向 deepmerge 函式本身 * - default: 指向預設匯出的 deepmerge * - isMergeable: 便捷函式 * - SYMBOL_IS_MERGEABLE: 符號匯出 * - deepmergeAll: 所有物件合併函式 * - all: deepmergeAll 的別名 * - _isMergeableObject: 內部可合併判斷函式 * * Adds necessary properties for non-ESM environments: * - __esModule: Mark as ES module * - deepmerge: Point to deepmerge function itself * - default: Point to default exported deepmerge * - isMergeable: Convenience function * - SYMBOL_IS_MERGEABLE: Symbol export * - deepmergeAll: All objects merge function * - all: Alias for deepmergeAll * - _isMergeableObject: Internal mergeable check function */ // @ts-ignore if (process.env.TSDX_FORMAT !== 'esm') { /** * CommonJS 環境兼容性處理 * 為模組添加必要的屬性以支援 CommonJS 匯入方式 * CommonJS environment compatibility handling * Add necessary properties to support CommonJS import methods * * 例如:const deepmerge = require('deepmerge-plus') * 或 :import * as deepmerge from 'deepmerge-plus' */ /** 標記為 ES 模組 / Mark as ES module */ Object.defineProperty(deepmerge, "__esModule", { value: true }); /** 匯出 deepmerge 函式 / Export deepmerge function */ Object.defineProperty(deepmerge, 'deepmerge', { value: deepmerge }); /** 設定預設匯出 / Set default export */ Object.defineProperty(deepmerge, 'default', { value: deepmerge }); /** 匯出便捷函式 isMergeable / Export convenience function isMergeable */ Object.defineProperty(deepmerge, 'isMergeable', { value: isMergeable }); /** 匯出可合併物件標記符號 / Export mergeable object marker symbol */ Object.defineProperty(deepmerge, 'SYMBOL_IS_MERGEABLE', { value: SYMBOL_IS_MERGEABLE }); /** 匯出 deepmergeAll 函式 / Export deepmergeAll function */ Object.defineProperty(deepmerge, 'deepmergeAll', { value: deepmergeAll }); /** 匯出 all 別名 / Export all alias */ Object.defineProperty(deepmerge, 'all', { value: deepmergeAll }); /** 匯出內部函式 _isMergeableObject / Export internal function _isMergeableObject */ Object.defineProperty(deepmerge, '_isMergeableObject', { value: _isMergeableObject }); }