文章目录
- 前言
- 问题背景
- 实现拷贝表单
- 如何实现深拷贝
- Object.assign
- JSON实现的深拷贝
- 递归实现
- 解决循环引用的递归实现
- require('lodash').cloneDeep()
前言
在做复杂的动态表单,实现业务动态变动,比如有一条需要动态添加的el-form-item中包含了多个输入框,并实现表单验证,但在element-ui组件库中给出的表单校验中没有这样的格式,解决方法可参考文章:Element-UI 实现动态增加多个输入框并校验。
如果不想要固定格式的动态增加表单,且增加表单的类型不同,比如按钮开关、文本输入框、数字输入框,想要自由增加不同类型的表单并验证,可以参考文章:Element-UI 实现动态增加多个不同类型的输入框并校验(双重v-for表单验证)。
如果还想要还要复制多套表单且可编辑,可以参考本文。
问题背景
在复制表单之后,对表单进行修改,发现所有表单的值都同时改变,分析:表单没有进行深拷贝,而是引用的其它表单的值。
实现拷贝表单
表单内容:
1、第一步坑🤔
在复制表单按钮的方法添加代码:
this.form.content.push(this.form.content[index])
复制,编辑,啊这。。。多套表单内容同时改变
2、第二步坑😥
this.form.content.push([...this.form.content[index]])
还是一样,...
深拷贝最外一层,内层还是引用。
3、实现
解决方法有两种:
① 序列化,再反序列化
- 序列化:把对象转换为字节序列的过程称为对象的序列化
- 反序列化:把字节序列恢复为对象的过程称为对象的反序列化
this.form.content.push(JSON.parse(JSON.stringify(this.form.content[index])))
② require(‘lodash’).cloneDeep()
this.form.content.push(require('lodash').cloneDeep(this.form.content[index]))
既然如此,那就不得不重新学习深拷贝了。
如何实现深拷贝
Object.assign
Object.assign默认是对对象进行深拷贝的,但是我们需要注意的是,它只对最外层的进行深拷贝,也就是当对象内嵌套有对象的时候,被嵌套的对象进行的还是浅拷贝;
function cloneDeepAssign(obj){
return Object.assign({},obj)
}
温馨提示:数组拷贝方法当中,使用…、slice、concat等进行拷贝也是一样的效果,只深拷贝最外层
同时,我们知道Object.assign针对的是对象自身可枚举的属性,对于不可枚举的没有效果;
所以,当我们对于一个层次单一对象的时候,可以考虑这种方法,简单快捷。(不支持undefined拷贝)
JSON实现的深拷贝
这是我们最最最常提到的一种深拷贝的方式,一般大部分的深拷贝都可以用JSON的方式进行解决,本质是因为JSON会自己去构建新的内存来存放新对象。
先将需要拷贝的对象进行JSON字符串化,然后再pase解析出来,赋给另一个变量,实现深拷贝。
function cloneDeepJson(obj){
return JSON.parse(JSON.stringify(obj))
}
这个方法有一些弊端,我们要注意的是:
- 会忽略 undefined和symbol;
- 不可以对Function进行拷贝,因为JSON格式字符串不支持Function,在序列化的时候会自动删除;
- 诸如 Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型在进行序列化时会丢失;
- 不支持循环引用对象的拷贝;(循环引用的可以大概地理解为一个对象里面的某一个属性的值是它自己)
递归实现
function cloneDeepDi(obj){
const newObj = {};
let keys = Object.keys(obj);
let key = null;
let data = null;
for(let i = 0; i<keys.length;i++){
key = keys[i];
data = obj[key];
if(data && typeof data === 'object'){
newObj[key] = cloneDeepDi(data)
}else{
newObj[key] = data;
}
}
return newObj
}
虽然但是,它也是有缺点的,就是不能解决循环引用的问题,一旦出现了循环引用,就死循环了!
解决循环引用的递归实现
function deepCopy(obj, parent = null) {
// 创建一个新对象
let result = {};
let keys = Object.keys(obj),
key = null,
temp = null,
_parent = parent;
// 该字段有父级则需要追溯该字段的父级
while (_parent) {
// 如果该字段引用了它的父级则为循环引用
if (_parent.originalParent === obj) {
// 循环引用直接返回同级的新对象
return _parent.currentParent;
}
_parent = _parent.parent;
}
for (let i = 0; i < keys.length; i++) {
key = keys[i];
temp = obj[key];
// 如果字段的值也是一个对象
if (temp && typeof temp === 'object') {
// 递归执行深拷贝 将同级的待拷贝对象与新对象传递给 parent 方便追溯循环引用
result[key] = DeepCopy(temp, {
originalParent: obj,
currentParent: result,
parent: parent
});
} else {
result[key] = temp;
}
}
return result;
}
大致的思路是:判断一个对象的字段是否引用了这个对象或这个对象的任意父级,如果引用了父级,那么就直接返回同级的新对象,反之,进行递归的那套流程。
但其实,还有一种情况是没有解决的,就是子对象间的互相引用,使用时需注意。
require(‘lodash’).cloneDeep()
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false
这是最最最最完美的深拷贝的方式,它已经将会出现问题的各种情况都考虑在内了,所以在日常项目开发当中,建议使用这种成熟的解决方案。
注:其实lodash解决循环引用的方式,就是用一个栈记录所有被拷贝的引用值,如果再次碰到同样的引用值的时候,不会再去拷贝一遍,而是利用之前已经拷贝好的。
进阶:Vue 复杂json数据在el-table表格中展示(el-table分割数据)