一、 前言
“JSON對象合並”是前端開發和 NodeJS 環境開發中非常常見的操作。開發者通常會通過循環遍歷或一些庫封裝的方法或 JavaScript ECMAScript 2015 定義的 Object.assign() 來實現。
二、 常見合並方式
1. 方法一:循環遍歷法
function extend() { var length = arguments.length; if(length == 0)return {}; if(length == 1)return arguments[0]; var target = arguments[0] || {}; for (var i = 1; i < length; i++) { var source = arguments[i]; for (var key in source) { if (source.hasOwnProperty(key)) { target[key] = source[key]; } } } return target; } const obj1 = { a: 1, b: 2 }; const obj2 = { b: 3, c: 4, d: 5 }; const result = extend(obj1, obj2); console.log(result);//{ a: 1, b: 3, c: 4, d: 5 }
2. 方法二:Object.assign()
const obj1 = { a: 1, b: 2 }; const obj2 = { b: 3, c: 4, d: 5 }; const result = Object.assign(obj1, obj2); console.log(result);//{ a: 1, b: 3, c: 4, d: 5 }
3. 方法三:用jQuery插件
const jsdom = require("jsdom"); const { JSDOM } = jsdom; const { window } = new JSDOM(`<!DOCTYPE html>`); const $ = require('jQuery')(window); const obj1 = { a: 1, b: 2 }; const obj2 = { b: 3, c: 4, d: 5 }; const result = $.extend(obj1, obj2); console.log(result);//{ a: 1, b: 3, c: 4, d: 5 }
從測試結果來看,以上三種方案都能得到預期結果。
三、 進階
但是在實際項目中,需要合並的對象往往不是那么簡單,比如合並以下幾個對象:
let chartScheme = { series: [{ name: '本地', type: 'bar', stack: '駐留分析', }, { name: '省內', type: 'bar', stack: '駐留分析', } ] };
let chartStyle = { grid: { top: '25%', bottom: '30', left: '15%', right: '5%' }, series: [{ barWidth: '10px', itemStyle: { color: '#4AC6E7' } }, { barWidth: '10px', itemStyle: { color: '#73C642' } } ] };
let chartStyle2 = { series: [{ lineStyle: { normal: { color: '#36d0f3' } }, itemStyle: { normal: { color: '#36d0f3' } } }, { lineStyle: { normal: { color: '#36d0f3' } }, itemStyle: { normal: { color: '#36d0f3' } } } ] };
let chartData = { series: [{ data: [35, 60, 50, 80, 80, 55, 70, 70], }, { data: [60, 80, 60, 45, 80, 55, 60, 20], } ] };
合並的目標是保留所有非同名屬性,同名原子屬性用后面的參數覆蓋前面的參數。
下面用上述幾種方法做合並測試:
1. 測試方法一
let result = extend(chartScheme, chartStyle, chartStyle2, chartData);
console.log(result);
結果如下,非原子屬性被覆蓋,未達到要求:
result = { "series": [{ "data": [35, 60, 50, 80, 80, 55, 70, 70] }, { "data": [60, 80, 60, 45, 80, 55, 60, 20] }], "grid": { "top": "25%", "bottom": "30", "left": "15%", "right": "5%" } }
2. 測試方法二
let result = Object.assign(chartScheme, chartStyle, chartStyle2, chartData);
console.log(result);
結果如下,跟方法一一樣未達到要求:
result = { "series": [{ "data": [35, 60, 50, 80, 80, 55, 70, 70] }, { "data": [60, 80, 60, 45, 80, 55, 60, 20] }], "grid": { "top": "25%", "bottom": "30", "left": "15%", "right": "5%" } }
3. 測試方法三
let result = $.extend(chartScheme, chartStyle, chartStyle2, chartData);
console.log(JSON.stringify(result));
結果如下,還是沒有到要求:
result = { "series": [{ "data": [35, 60, 50, 80, 80, 55, 70, 70] }, { "data": [60, 80, 60, 45, 80, 55, 60, 20] }], "grid": { "top": "25%", "bottom": "30", "left": "15%", "right": "5%" } }
很顯然,以上三種方法在合並比較復雜的對象時都是有缺陷的,合並后目標對象的非原子屬性被后面的源對象的同名屬性直接覆蓋了。
不過 jQuery 的“extend()”方法有兩個原型:
$.extend( target [, object1 ] [, objectN ] )
$.extend( [deep ], target, object1 [, objectN ] )
上面用的是第一個原型,第二個原型還有一個深拷貝選項,即“$.extend()”的第一個參數傳“true”,可以試試:
let result = $.extend(true, chartScheme, chartStyle, chartStyle2, chartData); console.log(JSON.stringify(result));
結果如下:
let result = $.extend(true, chartScheme, chartStyle, chartStyle2, chartData); console.log(JSON.stringify(result)); result = { "series": [{ "name": "本地", "type": "bar", "stack": "駐留分析", "barWidth": "10px", "itemStyle": { "color": "#4AC6E7", "normal": { "color": "#36d0f3" } }, "lineStyle": { "normal": { "color": "#36d0f3" } }, "data": [35, 60, 50, 80, 80, 55, 70, 70] }, { "name": "省內", "type": "bar", "stack": "駐留分析", "barWidth": "10px", "itemStyle": { "color": "#73C642", "normal": { "color": "#36d0f3" } }, "lineStyle": { "normal": { "color": "#36d0f3" } }, "data": [60, 80, 60, 45, 80, 55, 60, 20] }], "grid": { "top": "25%", "bottom": "30", "left": "15%", "right": "5%" } }
從結果來看,合並后的數據是完整的,並且合並后再修改源對象,並沒有影響到合並結果,這說明合並拷貝是深拷貝,並不是引用拷貝,是一款不錯的 API。但由於 jQuery 庫在 node 環境使用還是比較麻煩的,在SPA 應用和移動端的效率也不太高,因此有必要整理一個較高效的庫供日常使用。
四、 高效可靠的合並方法
1. 關鍵代碼
經過對數個前端 js 庫的對比研究,整理了一個庫,關鍵代碼如下:
/** * @Description 克隆對象 * 能被克隆的對象類型: * Plain object, Array, TypedArray, number, string, null, undefined. * 直接用原始數據進行賦值的數據類型: * BUILTIN_OBJECT * 用戶定義類的實例將克隆到一個原型中沒有屬性的普通對象。 * @method clone * @param {*} source * @return {*} new */ clone(source) { if (source == null || typeof source !== 'object') { return source; } var result = source; var typeStr = this.objToString.call(source); if (typeStr === '[object Date]') { result = this.cloneDate(source); } else if (typeStr === '[object RegExp]') { result = this.cloneRegExp(source); } else if (typeStr === '[object Function]') { result = this.cloneFunction(source); } else if (typeStr === '[object Array]') { result = []; for (var i = 0, len = source.length; i < len; i++) { result[i] = this.clone(source[i]); } } else if (this.TYPED_ARRAY[typeStr]) { var Ctor = source.constructor; if (source.constructor.from) { result = Ctor.from(source); } else { result = new Ctor(source.length); for (var i = 0, len = source.length; i < len; i++) { result[i] = this.clone(source[i]); } } } else if (!this.BUILTIN_OBJECT[typeStr] && !this.isDom(source)) { result = {}; for (var key in source) { if (this.hasOwn(source, key)) { result[key] = this.clone(source[key]); } } } return result; }, /** * @Description 合並函數 * @method merge * @param {*} target * @param {*} source * @param {boolean} [overwrite=false] * @return {Object} */ merge(target, source, overwrite) { // We should escapse that source is string // and enter for ... in ... if (!this.isObject(source) || !this.isObject(target)) { return overwrite ? this.clone(source) : target; } for (var key in source) { if (this.hasOwn(source, key)) { var targetProp = target[key]; var sourceProp = source[key]; if (this.isObject(sourceProp) && this.isObject(targetProp) && !this.isDom(sourceProp) && !this.isDom(targetProp) && !this.isBuiltInObject(sourceProp) && !this.isBuiltInObject(targetProp) ) { // 如果需要遞歸覆蓋,就遞歸調用merge this.merge(targetProp, sourceProp, overwrite); } else if (overwrite || !(key in target)) { // 否則只處理overwrite為true,或者在目標對象中沒有此屬性的情況 // NOTE,在 target[key] 不存在的時候也是直接覆蓋 target[key] = this.clone(source[key], true); } } } return target; }
2. 完整代碼與庫
完整代碼在 http://192.168.x.y/z/merge-util,代碼已經經過處理,兼容 node 環境和瀏覽器環境,npm 包已經發布到公司內部庫:http://192.168.x.y:z/#browse/browse:merge-util。
五、 使用方法
1. Node環境
使用 npm 命令用臨時 registry 安裝包:
npm --registry http://192.168.x.y:z/my-repository/ install merge-util
或設置持久 registry 再安裝包:
npm config set registry http://192.168.x.y:z/my-repository/ npm install merge-util
const mergeUtil = require('merge-util'); let result = mergeUtil.mergeAll([{},chartScheme, chartStyle, chartStyle2, chartData], true); console.log(JSON.stringify( result));
2. 瀏覽器環境
通過上述 npm 命令將安裝包下載到本地,或從 github 下載 index.js 文件,然后在 html 文件里引用即可使用:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Merge-test</title> <script src="./rs-merge-util.js"></script> <script src="./testdata.js"></script> </head> <body> <script> const result = mergeUtil.mergeAll([{}, chartScheme, chartStyle, chartStyle2, chartData0, chartData], true); console.log(JSON.stringify(result)); </script> </body> </html>