合并JSON对象的正确方式
一、 前言
“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>