deepmerge-plus
Version:
用於深度(遞迴)合併 JavaScript 物件的函式庫 / A library for deep (recursive) merging of JavaScript objects
503 lines (389 loc) • 15.2 kB
Markdown
# 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 實作分析 |