deepmerge-plus
Version:
用於深度(遞迴)合併 JavaScript 物件的函式庫 / A library for deep (recursive) merging of JavaScript objects
342 lines (270 loc) • 8.49 kB
Markdown
# keyValueUpsertMode 與陣列合併策略
# keyValueUpsertMode with Array Merge Strategies
## 概述
本文件說明 `keyValueUpsertMode` 選項與不同陣列合併策略的組合使用方式,並提供實用場景範例。
This document explains how to combine `keyValueUpsertMode` option with different array merge strategies, with practical examples.
## 陣列合併策略
deepmerge 支援多種陣列合併策略,透過 `arrayMerge` 選項自訂。
deepmerge supports multiple array merge strategies through the `arrayMerge` option.
### 策略比較表
| 策略 | 函式 | 行為 | 適用情境 |
|------|------|------|---------|
| **預設串接** | (不指定) | `[...target, ...source]` | 兩者合併,保留所有元素 |
| **左邊為主** | `arrayMergeTargetWins` | `target` | 保留預設值,忽略新資料 |
| **右邊為主** | `arrayMergeSourceWins` | `source` | 完全覆蓋,使用新資料 |
### 策略程式碼
```typescript
/**
* 預設串接:合併兩邊陣列
* Default concat: Merge both arrays
*/
function defaultArrayMerge(target: any[], source: any[]): any[]
{
return target.concat(source);
}
/**
* 左邊為主:完全保留目標陣列
* Left primary: Keep target array completely
*/
function arrayMergeTargetWins(target: any[], source: any[]): any[]
{
return target;
}
/**
* 右邊為主:完全使用來源陣列
* Right primary: Use source array completely
*/
function arrayMergeSourceWins(target: any[], source: any[]): any[]
{
return source;
}
```
## 與 keyValueUpsertMode 搭配
### keyValueUpsertMode: true 的作用
當 `keyValueUpsertMode: true` 時:
- 如果目標值不是 `undefined`,保留目標值
- 如果目標值是 `undefined`,使用來源值
- 對於陣列,這會影響整個陣列是否被採用
When `keyValueUpsertMode: true`:
- If target value is not `undefined`, keep target value
- If target value is `undefined`, use source value
- For arrays, this affects whether the entire array is used
### 三種策略對比
```typescript
const target = {
tags: ['a', 'b', 'c'],
name: 'Alice',
};
const source = {
tags: ['x', 'y', 'z'],
name: 'Bob',
};
// 三種策略結果
const resultTargetWins = merge(target, source, {
keyValueUpsertMode: true,
arrayMerge: arrayMergeTargetWins,
});
// tags = ['a', 'b', 'c'] (左邊為主-目標)
// name = 'Alice' (keyValueUpsertMode 保留)
const resultSourceWins = merge(target, source, {
keyValueUpsertMode: true,
arrayMerge: arrayMergeSourceWins,
});
// tags = ['x', 'y', 'z'] (右邊為主-來源)
// name = 'Alice' (keyValueUpsertMode 保留)
const resultConcat = merge(target, source, {
keyValueUpsertMode: true,
});
// tags = ['a', 'b', 'c', 'x', 'y', 'z'] (預設串接)
// name = 'Alice' (keyValueUpsertMode 保留)
```
### 結果對照表
| 策略 | `tags` 結果 | `name` 結果 |
|------|-------------|-------------|
| `arrayMergeTargetWins` | `['a', 'b', 'c']` | `'Alice'` |
| `arrayMergeSourceWins` | `['x', 'y', 'z']` | `'Alice'` |
| 預設串接 | `['a', 'b', 'c', 'x', 'y', 'z']` | `'Alice'` |
## 實用場景
### 場景 1:使用者配置與預設配置合併
```typescript
/**
* 情境:合併使用者配置與預設配置
* - 使用者已設定的值應該被保留
* - 新的設定應該被添加
* - 陣列(如外掛清單)應該完全替換而非合併
*/
const defaultConfig = {
plugins: ['analytics', 'seo', 'security'],
theme: 'light',
language: 'en',
};
const userConfig = {
plugins: ['custom-plugin'],
theme: 'dark',
};
// 使用 arrayMergeSourceWins:使用者的外掛清單完全替換預設清單
const result = merge(defaultConfig, userConfig, {
keyValueUpsertMode: true,
arrayMerge: arrayMergeSourceWins,
});
// 結果:
// {
// plugins: ['custom-plugin'], // 完全替換
// theme: 'light', // 保留預設(keyValueUpsertMode)
// language: 'en' // 新增鍵
// }
```
### 場景 2:API 回應與快取合併
```typescript
/**
* 情境:將 API 回應與本地快取合併
* - 已經存在的資料應該被保留(以快取為準)
* - 新資料應該被添加
* - 歷史記錄陣列應該完全替換(只看最新)
*/
const cache = {
history: ['action-1', 'action-2', 'action-3'],
profile: {
name: 'Cached User',
updatedAt: '2024-01-01',
},
};
const apiResponse = {
history: ['action-4'],
profile: {
name: 'Cached User',
updatedAt: '2024-01-15',
},
};
const result = merge(cache, apiResponse, {
keyValueUpsertMode: true,
arrayMerge: arrayMergeSourceWins,
});
// 結果:
// {
// history: ['action-4'], // 完全替換(只看最新)
// profile: {
// name: 'Cached User', // 保留快取值(keyValueUpsertMode)
// updatedAt: '2024-01-01', // 保留舊時間(keyValueUpsertMode)
// },
// }
```
### 場景 3:表單預設值填充
```typescript
/**
* 情境:用預設值填充表單
* - 使用者已填寫的值應該被保留
* - 未填寫的欄位使用預設值
* - 選項陣列應該完全替換(多選題)
*/
const defaultValues = {
interests: ['reading', 'music', 'sports'],
notifications: ['email', 'sms'],
name: undefined,
};
const userInput = {
interests: ['coding'],
notifications: ['push'],
name: 'John',
};
const result = merge(defaultValues, userInput, {
keyValueUpsertMode: true,
arrayMerge: arrayMergeSourceWins,
});
// 結果:
// {
// interests: ['coding'], // 完全替換
// notifications: ['push'], // 完全替換
// name: 'John' // 使用使用者輸入(因為預設是 undefined)
// }
```
### 場景 4:預設值優先
```typescript
/**
* 情境:系統預設值應該優先於使用者輸入
* - 某些關鍵設定不允許使用者修改
* - 使用 arrayMergeTargetWins 確保預設值不被覆蓋
*/
const systemDefaults = {
allowedPlugins: ['official-plugin'],
maxUploadSize: 100,
features: ['feature-a', 'feature-b'],
};
const userSettings = {
allowedPlugins: ['hacked-plugin'], // 嘗試添加惡意插件
maxUploadSize: 10000, // 嘗試增加上傳限制
features: ['feature-c'], // 嘗試添加功能
};
// 使用 arrayMergeTargetWins:系統預設值優先
const result = merge(systemDefaults, userSettings, {
keyValueUpsertMode: true,
arrayMerge: arrayMergeTargetWins,
});
// 結果:
// {
// allowedPlugins: ['official-plugin'], // 保持預設
// maxUploadSize: 100, // 保持預設
// features: ['feature-a', 'feature-b'], // 保持預設
// }
```
## 巢狀物件中的陣列行為
所有策略都會遞迴應用於巢狀物件中的陣列。
All strategies are recursively applied to arrays in nested objects.
```typescript
const target = {
config: {
plugins: ['plugin-a', 'plugin-b'],
modules: ['mod-1', 'mod-2'],
},
};
const source = {
config: {
plugins: ['plugin-c'],
modules: ['mod-3'],
},
};
const result = merge(target, source, {
keyValueUpsertMode: true,
arrayMerge: arrayMergeSourceWins,
});
// 巢狀陣列也會被替換:
// {
// config: {
// plugins: ['plugin-c'], // 完全替換
// modules: ['mod-3'], // 完全替換
// },
// }
```
## 注意事項
### 1. keyValueUpsertMode 與陣列的互動
當目標陣列為 `undefined` 時:
- `keyValueUpsertMode: true` 會使用來源陣列
- 之後再由 `arrayMerge` 策略決定合併方式
When target array is `undefined`:
- `keyValueUpsertMode: true` will use source array
- Then `arrayMerge` strategy decides how to merge
### 2. 空陣列的處理
| 情境 | `arrayMergeNoConcat` | `arrayMergeTargetWins` | 預設串接 |
|------|---------------------|----------------------|---------|
| target=`[]`, source=`['a']` | `['a']` | `[]` | `['a']` |
| target=`['a']`, source=`[]` | `[]` | `['a']` | `['a']` |
| target=`[]`, source=`[]` | `[]` | `[]` | `[]` |
### 3. 效能考量
- `arrayMergeTargetWins` 和 `arrayMergeSourceWins` 最快(直接回傳)
- `arrayMergeNoConcat` 次之(需複製來源陣列)
- 預設串接最慢(需合併兩個陣列)
## 相關檔案
- `test/options/key-value-upsert-mode-no-array-merge.test.ts` - 測試檔案
- `src/index.ts` - 核心實作
- `docs/key-value-upsert-mode-analysis.md` - keyValueUpsertMode 分析文件
## 更新日誌
| 日期 | 更新內容 |
|------|---------|
| 2026-03-26 | 新增 keyValueUpsertMode 與陣列合併策略文件 |