UNPKG

deepmerge-plus

Version:

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

503 lines (389 loc) 15.2 kB
# keyValueUpsertMode 實作分析 # keyValueUpsertMode Implementation Analysis --- ## 概述 / Overview 本文分析 `keyValueUpsertMode` 選項的三種不同實作方式,比較其優缺點、適用情境,並與 lodash 函式進行結果對比。 This document analyzes three different implementation approaches for the `keyValueUpsertMode` option, comparing their pros and cons, applicable scenarios, and results against lodash functions. --- ## 三種實作方式 / Three Implementation Approaches ### Attempt 1: `keyValueUpsertMode: true` ```typescript /** * 合併兩個物件 * merge(userConfig, defaultConfig, { keyValueUpsertMode: true }) * * 行為 / Behavior: * - userConfig[key] !== undefined 時,保留 userConfig 的值 * - userConfig[key] === undefined 時,使用 defaultConfig 的值 */ const result = merge(userConfig, defaultConfig, { keyValueUpsertMode: true, }); ``` ### Attempt 2: `keyValueUpsertMode` 自訂函式 ```typescript /** * 使用自訂函式控制合併行為 * merge(userConfig, defaultConfig, { keyValueUpsertMode: (value, options, tmpRuntime) => ... }) */ const result = merge(userConfig, defaultConfig, { keyValueUpsertMode: (value, options, tmpRuntimeTarget) => { const userConfigValue = tmpRuntimeTarget?.target?.[tmpRuntimeTarget.key as string]; return userConfigValue !== undefined; }, }); ``` --- ## 與 lodash 函式對比 / Comparison with Lodash Functions ### 1. _.defaults (淺層預設值) | 實作方式 | lodash 結果比較 | 說明 | |---------|-------------------|------| | Attempt 1: `keyValueUpsertMode: true` | ⚠️ 不同 | 會遞迴合併巢狀物件,添加 source 中的新鍵 | | Attempt 2: 自訂函式 | ⚠️ 不同 | 仍會遞迴合併巢狀物件 | **lodash _.defaults 行為**: - 只填充第一層缺失的鍵 - 不會遞迴處理巢狀物件 - 完全不覆蓋 target 中已存在的值 **keyValueUpsertMode: true 行為**: - 會遞迴合併巢狀物件 - target[key] !== undefined 時保留 target - target[key] === undefined 時使用 source - 會將 source 中的新鍵(包括巢狀物件中的新鍵)添加到結果中 **差異說明 / Difference Explanation**: - `_.defaults({ a: { b: 1 } }, { a: { c: 2 } })` `{ a: { b: 1 } }`(只填充第一層,不處理巢狀) - `merge({ a: { b: 1 } }, { a: { c: 2 } }, { keyValueUpsertMode: true })` `{ a: { b: 1, c: 2 } }`(遞迴合併) **為什麼不同 / Why Different**: - lodash _.defaults 是**淺層**操作,只處理第一層 - keyValueUpsertMode 是**深度**操作,會遞迴合併 **好處 / Benefits**: - keyValueUpsertMode 可以合併深層巢狀物件 - 適合多層級配置的合併需求 **適用情境 / Applicable Scenarios**: - 需要合併深層巢狀物件的 defaults - 配置物件有多層級結構 --- ### 2. _.defaultsDeep (深度預設值) | 實作方式 | lodash 結果比較 | 說明 | |---------|-------------------|------| | Attempt 1: `keyValueUpsertMode: true` | ⚠️ 不同 | 兩者都會遞迴合併,但處理方式不同 | | Attempt 2: 自訂函式 | ⚠️ 不同 | 仍會遞迴合併巢狀物件 | **lodash _.defaultsDeep 行為**: - 會遞迴填充缺失的鍵 - 不會覆蓋已存在的值 - 語義:填充缺失(fill missing) **keyValueUpsertMode: true 行為**: - 會遞迴合併物件 - target[key] !== undefined 時保留 target - target[key] === undefined 時使用 source - 不覆蓋已存在的值(除非是 undefined) - 語義:有則保留(preserve if exists) **差異說明 / Difference Explanation**: - `_.defaultsDeep({ a: { b: 1 } }, { a: { b: 2, c: 3 } })` `{ a: { b: 1, c: 3 } }`(只填充缺失,不覆蓋現有) - `merge({ a: { b: 1 } }, { a: { b: 2, c: 2 } }, { keyValueUpsertMode: true })` `{ a: { b: 1, c: 2 } }` - 兩者結果相同!差異在於**語義**: - _.defaultsDeep:確保有值(fill missing) - keyValueUpsertMode:保留現有(preserve existing) **為什麼不同 / Why Different**: - 語義上的差異:fill missing vs preserve existing - 雖然結果可能相同,但概念不同 **好處 / Benefits**: - 可實現「保留優先值」的概念 - 更適合配置繼承場景 **適用情境 / Applicable Scenarios**: - 配置繼承(基礎配置 + 環境配置) - 使用者偏好設定覆蓋預設值 --- ### 3. _.merge (深度合併) | 實作方式 | lodash 結果比較 | 說明 | |---------|-------------------|------| | `keyValueUpsertMode: false` | 相似 | 使用預設深度合併行為 | | `keyValueUpsertMode: true` | ⚠️ 不同 | 會保留 target undefined | **lodash _.merge 行為**: - 會遞迴合併巢狀物件 - 會覆蓋所有值(不保留 target 的值) - 陣列處理:替換(replace) **keyValueUpsertMode: false 行為**: - 會遞迴合併巢狀物件 - 會覆蓋所有值 - 陣列處理:串接(concat)- 預設行為 **keyValueUpsertMode: true 行為**: - 會遞迴合併巢狀物件 - target[key] === undefined 時使用 source - 不會覆蓋已存在的值 **差異說明 / Difference Explanation**: - `_.merge({ a: { b: 1 } }, { a: { b: 2 } })` `{ a: { b: 2 } }`(覆蓋) - `merge({ a: { b: 1 } }, { a: { b: 2 } }, { keyValueUpsertMode: true })` `{ a: { b: 1 } }`(保留) **為什麼不同 / Why Different**: - _.merge:覆蓋所有值(overwrite all) - keyValueUpsertMode: true:保留現有值(preserve existing) **好處 / Benefits**: - 可實現「優先使用現有值」的概念 - 適合「設定優先順序」的場景 **適用情境 / Applicable Scenarios**: - 保留使用者設定,只填充未設定的選項 - 配置繼承(後面的設定不覆蓋前面的) --- ## 各實作方式優缺點分析 / Pros and Cons Analysis ### Attempt 1: `keyValueUpsertMode: true` **優點 / Pros**: - 語法簡單,易於使用 - 可正確處理 undefined - 會遞迴合併巢狀物件,添加 source 中的新鍵 - 保留 falsy 值(0, false, '' **缺點 / Cons**: - _.defaults 的主要差異在於:lodash 只處理第一層,keyValueUpsertMode 會遞迴合併 - keyValueUpsertMode 會將 source 中的新鍵添加到結果中 - lodash _.defaults 不會遞迴處理巢狀物件 **適用情境 / Applicable Scenarios**: - 需要遞迴合併的深度 defaults - 配置合併:使用者設定覆蓋預設設定 - 需要保留 falsy 值的場景 --- ### Attempt 2: 自訂函式 **優點 / Pros**: - 可精確控制何時保留目標值 - 可根據鍵名、值、或其他條件自訂邏輯 - 彈性最大 **缺點 / Cons**: - 程式碼較複雜 - 仍無法完全模擬 lodash 行為(因為 deepmerge 會遞迴合併) - 需要了解內部 API **適用情境 / Applicable Scenarios**: - 需要根據鍵名決定是否覆蓋 - 需要根據多個條件綜合判斷 - 需要實現複雜的合併邏輯 --- ### Attempt 3: 實際應用場景 **優點 / Pros**: - 可實現「保留優先值」的功能 - 參數順序直觀 - 適合實際業務需求 **缺點 / Cons**: - 需要改變思維方式(將優先值放在 target) - lodash 行為方向相反 **適用情境 / Applicable Scenarios**: - 配置繼承(基礎配置 + 環境配置) - 表單預設值填充 - 使用者偏好設定 --- ## 最佳實作選擇 / Best Implementation Selection ### 最接近 lodash 實作概念 / Closest to Lodash Implementation **_.defaultsDeep** | 標準 | 選擇 | |-----|------| | 最接近 lodash 概念 | ⚠️ 部分接近 | | 原因 | 兩者都會遞迴處理,語義相似 | **說明**: - lodash _.defaultsDeep:填充缺失的鍵(fill missing) - keyValueUpsertMode:在第一個參數為 undefined 時使用第二個參數的值 兩者的語義對比: - _.defaultsDeep:確保有值(ensure value exists) - keyValueUpsertMode:有值則保留(preserve if exists) --- ### 各場景的最佳實作 / Best Implementation for Each Scenario | 場景 | 最佳實作 | 說明 | |------|---------|------| | 配置合併(保留使用者設定) | merge(userConfig, defaultConfig, { keyValueUpsertMode: true }) | | | 表單資料填充 | merge(userInput, defaultValues, { keyValueUpsertMode: true }) | | | 環境變數覆蓋 | merge(envConfig, baseConfig, { keyValueUpsertMode: true }) | | | 深度合併(類似 _.merge) | merge(userConfig, defaultConfig) merge(userConfig, defaultConfig, { keyValueUpsertMode: false }) | | | 自訂合併邏輯 | merge(userConfig, defaultConfig, { keyValueUpsertMode: (value, options, tmpRuntime) => ... }) | | --- ## 詳細比較表 / Detailed Comparison Table ### 與 _.defaults 比較 | 特性 | _.defaults | keyValueUpsertMode: true | |------|------------|-------------------------| | 填充第一層缺失的鍵 | | | | 覆蓋已存在的值 | | 否(除非是 undefined)| | 遞迴處理巢狀物件 | 否(只第一層) | | | 會添加 source 中的新鍵 | | | | 參數順序 | _.defaults(o1, o2, o3) | merge(target, source) | **差異說明**: - _.defaults:只處理第一層,巢狀物件不會被合併 - keyValueUpsertMode:會遞迴合併,添加 source 中的新鍵 **為什麼不同**: lodash 是淺層操作,keyValueUpsertMode 是深度操作 **好處**: - 可合併深層巢狀物件 - 適合多層級配置 **適用情境**: - 深度 defaults ### 與 _.defaultsDeep 比較 | 特性 | _.defaultsDeep | keyValueUpsertMode: true | |------|----------------|-------------------------| | 遞迴填充缺失的鍵 | | | | 覆蓋已存在的值 | | 否(除非是 undefined)| | 遞迴處理巢狀物件 | | | | 語義 | 填充缺失(fill missing) | 有則保留(preserve if exists)| **差異說明**: - 兩者都會遞迴處理,結果可能相同 - 差異在於**語義**而非行為 - _.defaultsDeep:確保有值(fill missing) - keyValueUpsertMode:保留現有(preserve if exists) **為什麼不同**: 語義上的差異:fill missing vs preserve existing **好處**: - 可實現「保留優先值」的概念 - 更適合配置繼承場景 **適用情境**: - 配置繼承(基礎配置 + 環境配置) - 使用者偏好設定覆蓋預設值 ### 與 _.merge 比較 | 特性 | _.merge | keyValueUpsertMode: false | keyValueUpsertMode: true | |------|---------|--------------------------|-------------------------| | 遞迴合併巢狀物件 | | | | | 覆蓋所有值 | | | | | 陣列處理 | 替換 | 串接 | 串接 | | 保留 undefined | | | | **差異說明**: - _.merge 會覆蓋所有值 - keyValueUpsertMode: true 只會填充 undefined 的值 **為什麼不同**: 覆蓋所有值 vs 保留現有值 **好處**: - 可實現「優先使用現有值」的概念 - 適合「設定優先順序」的場景 **適用情境**: - 保留使用者設定,只填充未設定的選項 - 配置繼承(後面的設定不覆蓋前面的) --- ## 實際應用範例 / Practical Application Examples ### 1. 配置合併 ```typescript // 預設配置 const defaultConfig = { theme: 'light', language: 'en', api: { url: 'https://api.example.com', timeout: 5000, }, }; // 使用者自訂配置 const userConfig = { theme: 'dark', // 使用者自訂 api: { timeout: 10000, // 使用者自訂 }, }; /** * 使用 keyValueUpsertMode: true * 將使用者設定作為 target,預設值作為 source * * 結果: * - theme: 'dark' (使用者設定) * - language: 'en' (預設值) * - api.url: 'https://api.example.com' (預設值) * - api.timeout: 10000 (使用者設定) */ const result = merge(userConfig, defaultConfig, { keyValueUpsertMode: true, }); ``` ### 2. 表單資料填充 ```typescript // 預設值 const defaultValues = { username: '', email: '', profile: { firstName: '', lastName: '', age: undefined, bio: '', }, }; // 使用者輸入 const userInput = { username: 'john_doe', email: 'john@example.com', profile: { firstName: 'John', lastName: 'Doe', bio: 'Hello world', }, }; /** * 結果: * - username: 'john_doe' (使用者輸入) * - email: 'john@example.com' (使用者輸入) * - profile.firstName: 'John' (使用者輸入) * - profile.lastName: 'Doe' (使用者輸入) * - profile.age: undefined (預設值) * - profile.bio: 'Hello world' (使用者輸入) */ const result = merge(userInput, defaultValues, { keyValueUpsertMode: true, }); ``` ### 3. 環境配置繼承 ```typescript // 基礎配置 const baseConfig = { api: { url: 'https://api.example.com', timeout: 5000, retries: 3, }, logging: { level: 'info', format: 'json', }, }; // 環境配置 const envConfig = { api: { url: 'https://api-staging.example.com', // timeout retries 未設定,使用 base 的值 }, logging: { level: 'debug', // format 未設定,使用 base 的值 }, }; /** * 結果: * - api.url: 'https://api-staging.example.com' (環境配置) * - api.timeout: 5000 (基礎配置) * - api.retries: 3 (基礎配置) * - logging.level: 'debug' (環境配置) * - logging.format: 'json' (基礎配置) */ const result = merge(envConfig, baseConfig, { keyValueUpsertMode: true, }); ``` --- ## 結論 / Conclusion ### keyValueUpsertMode 與 lodash 的核心差異 1. **遞迴處理方式不同**: - lodash _.defaults:只填充第一層缺失的鍵 - keyValueUpsertMode:會遞迴合併巢狀物件 2. **語義上的差異**: - lodash _.defaults:填充缺失的鍵(fill missing) - keyValueUpsertMode:有則保留(preserve if exists) 3. **無法完全匹配**: - keyValueUpsertMode 無法完全模擬 _.defaults _.defaultsDeep - 這是設計上的差異,而非實作問題 4. **適用場景不同**: - lodash:適用於需要完全不覆蓋的場景 - keyValueUpsertMode:適用於需要「有值則保留,無值則填充」的場景 - lodash:適用於需要完全不覆蓋的場景 - keyValueUpsertMode:適用於需要「有值則保留,無值則填充」的場景 ### 推薦使用方式 | 需求 | 推薦方式 | |------|---------| | 保留使用者設定 | merge(userConfig, defaultConfig, { keyValueUpsertMode: true }) | | 表單預設值填充 | merge(userInput, defaultValues, { keyValueUpsertMode: true }) | | 配置繼承 | merge(envConfig, baseConfig, { keyValueUpsertMode: true }) | | 深度合併 | 直接使用 merge() merge(target, source, { keyValueUpsertMode: false }) | | 自訂邏輯 | 使用 keyValueUpsertMode 函式 | ### 總結 - **最接近 lodash 概念**:無法完全匹配 - **最佳實作(實際應用)**:Attempt 1 + 正確的參數順序 - **keyValueUpsertMode 的價值**:不是用來模擬 lodash,而是提供一種新的合併策略 --- ## 更新日誌 / Changelog | 日期 | 更新內容 | |------|---------| | 2026-03-25 | 初始版本,建立 keyValueUpsertMode 實作分析 |