UNPKG

deepmerge-plus

Version:

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

376 lines (270 loc) 9.28 kB
# deepmerge-plus > 深度合併兩個 JavaScript 物件的可列舉屬性 / Merge the enumerable attributes of two objects deeply. `pnpm add deepmerge-plus` ## 概述 deepmerge-plus 是一個功能強大的 JavaScript 物件深度合併函式庫,支援: - depth-first 深度合併(遞迴處理巢狀物件) - 📦 陣列合併(可自訂合併策略) - ⚙️ 靈活的選項配置 - 🔧 自訂合併邏輯 --- ## 安裝 ```bash # 使用 pnpm(推薦) pnpm add deepmerge-plus # 使用 npm npm install deepmerge-plus # 使用 yarn yarn add deepmerge-plus ``` --- ## 快速開始 ```javascript const merge = require('deepmerge-plus'); const target = { foo: { bar: 5 }, array: [{ does: 'work', too: [1, 2, 3] }] }; const source = { foo: { baz: 4 }, quux: 5, array: [{ does: 'work', too: [4, 5, 6] }, { really: 'yes' }] }; const result = merge(target, source); console.log(result); // => { foo: { bar: 5, baz: 4 }, array: [...], quux: 5 } ``` --- ## 核心概念:左至右合併 `merge(target, source, options)` 中: | 參數 | 術語 | 說明 | |-----|------|------| | `target` | 左側(left)| 被合併的物件,代表**現有值** | | `source` | 右側(right)| 要合併進來的物件,代表**新值** | ### 預設行為 - **來源物件的值會覆蓋目標物件的值** - 合併結果是將「右側」的值合併進「左側」 - 合併會建立一個新物件,因此 `target` `source` 都不會被修改 ```javascript const target = { name: 'Alice', age: 25 }; const source = { name: 'Bob', city: 'Taipei' }; const result = merge(target, source); // => { name: 'Bob', age: 25, city: 'Taipei' } // name source 覆蓋,age 保留 target,city 來自 source ``` --- ## API ### merge(target, source, [options]) 深度合併兩個物件 `target` `source`,返回一個包含兩者元素的新合併物件。 ```javascript const result = merge(objectA, objectB, options); ``` ### merge.all(arrayOfObjects, [options]) 將任意數量的物件合併成單一結果物件。 ```javascript const result = merge.all([obj1, obj2, obj3], options); ``` --- ## 選項說明 ### 1. clone 是否啟用深度複製功能。 - **類型**: `boolean` - **預設值**: `true` ```javascript merge(target, source, { clone: true }); // 預設,深度複製物件 merge(target, source, { clone: false }); // 不複製,直接引用 ``` ### 2. arrayMerge 自訂陣列合併函式。 - **類型**: `function(target, source, options): array` - **預設行為**: 串接兩個陣列 ```javascript // 覆寫模式:來源陣列覆蓋目標陣列 function overwriteMerge(destinationArray, sourceArray) { return sourceArray; } merge([1, 2, 3], [3, 2, 1], { arrayMerge: overwriteMerge }); // => [3, 2, 1] ``` ```javascript // 僅保留目標陣列 const keepTarget = (destination) => destination; merge({ arr: [1,2,3] }, { arr: ['a','b','c'] }, { arrayMerge: keepTarget }); // => { arr: [1, 2, 3] } ``` ### 3. isMergeableObject 自訂可合併物件判斷函式。 - **類型**: `function(value): boolean` ```javascript const moment = require('moment'); merge(target, source, { isMergeableObject(value) { if (moment.isMoment(value)) return false; return isMergeableObject(value); } }); ``` ### 4. keyValueOrMode(已棄用) 使用 `||` 運算子邏輯,已由 `keyValueUpsertMode` 取代。 - **類型**: `boolean` - **狀態**: 已棄用,不建議在新程式碼中使用 ### 5. keyValueUpsertMode(推薦) Upsert 模式,控制何時應該保留目標中的現有值。 - **類型**: `boolean` `function` - **預設值**: `undefined` #### 為 `true` 時 保留目標中已存在的值(`undefined` 除外)。 ```javascript const target = { name: 'Alice', age: undefined, count: 0, email: null }; const source = { name: 'Bob', age: 30, count: 5, email: 'bob@example.com' }; const result = merge(target, source, { keyValueUpsertMode: true }); // => { name: 'Alice', age: 30, count: 0, email: null } // name: 保留 'Alice'(不是 undefined) // age: 使用 30(目標值是 undefined) // count: 保留 0(不是 undefined) // email: 保留 null(不是 undefined) ``` **關鍵特性**: - 使用 `??` 運算子,區分 `undefined` 與其他 falsy - `null`、`0`、`false`、`''` 都會被保留 - lodash `_.defaultsDeep` 概念相似 #### 為 `function` 時 自訂函式,根據條件決定是否保留目標的值。 ```javascript const target = { name: 'Alice', count: 0 }; const source = { name: 'Bob', count: 5 }; const result = merge(target, source, { keyValueUpsertMode: (value, options, tmpRuntimeTarget) => { const targetValue = tmpRuntimeTarget.target?.[tmpRuntimeTarget.key]; return targetValue === 0; // 只有當目標值為 0 時保留 } }); // => { name: 'Bob', count: 0 } // name: targetValue='Alice' !== 0,回傳 false,使用 source 'Bob' // count: targetValue=0 === 0,回傳 true,保留 target 0 ``` **函式參數說明**: | 參數 | 類型 | 說明 | |-----|------|------| | `value` | `unknown` | 來源物件中該鍵的值 | | `optionsRuntime` | `IOptions` | 目前的合併選項 | | `tmpRuntimeTarget` | `ICache` | 包含 `target`、`source`、`destination`、`key` 等資訊 | | `tmpRuntimeData` | `ITmpRuntimeData` | 包含 `level`、`paths`、`root`、`parent` 等資訊 | #### 進一步閱讀 如需了解 `keyValueUpsertMode` 與陣列合併策略的詳細組合應用,請參閱: - [keyValueUpsertMode 與陣列合併策略](./docs/examples/key-value-upsert-mode-array-merge-strategies.md) --- ## 實際應用場景 ### 1. 配置合併(保留使用者設定) ```javascript // 預設配置 const defaultConfig = { theme: 'light', language: 'en', notifications: { email: true, sms: false }, timeout: 3000 }; // 使用者自訂配置 const userConfig = { theme: 'dark', notifications: { email: false } }; // 合併:保留使用者設定,填充預設值 const finalConfig = merge(defaultConfig, userConfig, { keyValueUpsertMode: true }); // => { theme: 'dark', language: 'en', notifications: { email: false }, timeout: 3000 } ``` **對比 lodash**: - `_.defaults(userConfig, defaultConfig)` - 填充缺失的鍵 - `merge(defaultConfig, userConfig, { keyValueUpsertMode: true })` - 保留使用者設定 ### 2. 表單資料合併 ```javascript // 預設表單值 const defaultValues = { username: '', password: '', rememberMe: false, preferences: { newsletter: true, language: 'en' } }; // 使用者已輸入的資料 const userInput = { username: 'alice', preferences: { language: 'zh-TW' } }; // 合併:保留使用者已輸入的資料 const formData = merge(defaultValues, userInput, { keyValueUpsertMode: true }); // => { username: 'alice', password: '', rememberMe: false, preferences: { newsletter: true, language: 'zh-TW' } } ``` ### 3. 環境變數覆蓋 ```javascript // 基礎環境配置 const baseConfig = { API_URL: 'https://api.example.com', DEBUG: false, CACHE: { enabled: true, ttl: 3600 } }; // 開發環境配置 const devConfig = { DEBUG: true, API_URL: 'https://dev-api.example.com' }; // 生產環境配置 const prodConfig = { DEBUG: false }; // 合併環境配置(保留最基礎的值) const envConfig = merge(baseConfig, devConfig, { keyValueUpsertMode: true }); // => { API_URL: 'https://dev-api.example.com', DEBUG: true, CACHE: { enabled: true, ttl: 3600 } } ``` ### 4. 深度合併(類似 _.merge) ```javascript const target = { user: { name: 'Alice', age: 25 }, role: 'admin' }; const source = { user: { name: 'Bob', city: 'Taipei' }, role: 'user' }; // 預設深度合併(source 覆蓋 target) const result1 = merge(target, source); // => { user: { name: 'Bob', age: 25, city: 'Taipei' }, role: 'user' } // 使用 keyValueUpsertMode 保留 target 的值 const result2 = merge(target, source, { keyValueUpsertMode: true }); // => { user: { name: 'Alice', age: 25, city: 'Taipei' }, role: 'admin' } ``` --- ## 與 lodash 函式對比 | lodash 函式 | deepmerge-plus 實作 | 說明 | |------------|-------------------|------| | `_.defaults` | `merge(target, source, { keyValueUpsertMode: true })` | 淺層預設值(需正確參數順序) | | `_.defaultsDeep` | `merge(target, source, { keyValueUpsertMode: true })` | 深度預設值 | | `_.merge` | `merge(target, source)` | 預設深度合併 | **重要語義差異**: - `_.defaults` / `_.defaultsDeep`:填充缺失的鍵(fill missing) - `keyValueUpsertMode: true`:有值則保留(preserve if exists) --- ## 類型定義 ```typescript interface IOptions { /** 是否啟用深度複製,預設 true */ clone?: boolean; /** 自訂陣列合併函式 */ arrayMerge?: <T extends any[]>(target: T, source: any[], options?: IOptions) => T[]; /** 自訂可合併物件判斷函式 */ isMergeableObject?: (value: any) => boolean; /** 已棄用:使用 keyValueUpsertMode 取代 */ keyValueOrMode?: boolean; /** Upsert 模式:true 或自訂函式 */ keyValueUpsertMode?: | boolean | ((value: unknown, optionsRuntime?: IOptions, tmpRuntimeTarget?: ICache) => boolean); } ``` --- ## 測試 ```bash pnpm test ``` --- ## 授權 MIT