使用JSON.stringify()和JSON.parse()实现简单的深拷贝
1
2
3
4
5
6
7
8
9var obj = {
a:1,
b:'aaa',
c: {
d:'abc',
e:[1,2,3,4]
}}
var obj1 = JSON.parse(JSON.stringify(obj))
console.log(obj1) // { a: 1, b: 'aaa', c: { d: 'abc', e: [ 1, 2, 3, 4 ] } }以上使用JSON序列化就实现了一个简单的对象的深拷贝,但是使用JSON将一个对象序列化成文本的形式来实现深拷贝却是不那么完美的。这是因为JSON.stringify函数将一个js对象转换成文本化的JSON,然而如果遇到不能被转换成文本化的属性会被忽略掉,从而实现的拷贝是缺失的。
- 拷贝的对象中存在函数时,拷贝失败
1
2
3
4
5
6
7
8
9
10var obj = {
a:1,
b:'aaa',
c: {
d:'abc',
e:[1,2,3,4]
},
fn: function(){}}
var obj1 = JSON.parse(JSON.stringify(obj))
console.log(obj1) // { a: 1, b: 'aaa', c: { d: 'abc', e: [ 1, 2, 3, 4 ] } }
在控制台中运行以上的代码,我们发现经过JSON序列化时
obj.fn
无法被序列化,导致拷贝出来的数据obj1
丢失了obj.fn
这个函数从而拷贝失败.- 循环引用会导致拷贝失败
1
2
3
4
5
6
7
8
9
10var obj = {
a:obj1,
b:2
}
var obj1 = {
a:obj,
b:3
}
var obj2 = JSON.parse(JSON.stringify(obj1))
console.log(obj2) // { a: { b: 2 }, b: 3 }
在上面的代码中,我们看到,obj对象的a属性引用了obj1,而obj1中的a属性同时也引用了obj。在控制台中运行上述代码发现拷贝出来的对象obj2中a的属性只有对象
{b:2}
,丢失了a属性,从而导致拷贝失败。
3:无法拷贝属性值为Symbol和属性值为undefined的属性1
2
3
4
5
6
7var a = Symbol()
var obj = {
a:a,
b:undefined
}
var obj1 = JSON.parse(JSON.stringify(obj))
console.log(obj1) // {}我们声明了一个变量a为Symbole。然后将其赋值给
obj
对象的a
属性,给obj对象添加了一个属性值为undefined
的b
属性,然后使用JSON.stringify进行序列化,发现打印出来的是一个{}
对象,说明序列化失败.
4:在数组中,不能被序列化的元素会被使用null进行填充1
2
3const arr = [function(){},'a',undefined,'b',Symbol()]
const arr1 = JSON.parse(JSON.stringify(arr))
console.log(arr1) // [ null, 'a', null, 'b', null ]上面我们声明了一个数组
arr
,arr中有匿名函数,字符串,undefined以及Symbol,我们对arr进行序列化之后发现不能被序列化的元素都被使用null进行填充了,导致序列化失败。- 拷贝的对象中存在函数时,拷贝失败
遇到无法被序列化的属性我们可以重写对象的toJSON方法
绕过对象的某些属性无法被序列化的情况我们可以重写对象的toJSON方法来自定义被stringify的对象。在我们平时的开发中基本上每一次ajax请求都会使用JSON.stringify。而使用该方法就会把无法被序列化的属性给过滤掉。1
2
3
4
5
6
7
8
9
10
11class Person {
constructor(first,last){
this.first = first
this.last = last
}
toJSON(){
return {fullName: `${this.first} ${this.last}`}
}
}
const person = new Person('hello', 'world')
console.log(JSON.parse(JSON.stringify(person))) // { first: 'hello', last: 'world' }JSON.stringify的定义如下:
1
2
3
4
5
6
7
8
9
10
11
12JSON.stringify(value, replacer?, space?)
// 除了value值职位其余的参数都是可选的。
// replacer表示的是一个过滤函数或则一个数组包含要被stringify的属性名。如果没有定义,默认所有属性都被stringify。如果是函数的化,那么对象中的属性名和值会分别被传入进该函数中可以进行自定义。函数的返回值有以下几种情况:
// 返回undefined表示忽略该属性;
// 返回字符串,布尔值或则数字将会被stringify;
// 返回对象将会触发递归调用知道遇到基本类型的属性;
// 返回无法stringify的值将会被忽略;
// space可以使JSON.Stringify的输出格式更加的好看,例如可以使用tab(‘\t’)来将{a:1,b:2}进行分隔换行输出为以下形式
// {
// a:1,
// b:2
// }自己实现深拷贝
通过对JSON.stringify的分析我们知道使用JSON.stringify来实现深拷贝是存在问题的,那么在实际的项目中我们可能需要深拷贝的实现,这时候我们只能通过自己来实现深拷贝。- 首先实现以下浅拷贝
1
2
3
4
5
6
7
8
9
10
11function shallowCopy(obj){
var target = {}
for (var key in obj) {
target[key] = obj[key]
}
return target
}
// 测试一下
var obj = {a:1,b:2,c:{d:'hello', e: 'world'},fn:function(){}}
var target = shallowCopy(obj)
console.log(target) // { a: 1, b: 2, c: { d: 'hello', e: 'world' } }
以上我们实现了一个简单的浅拷贝,下面我们修改一下
target
的值,然后再看看obj中的值是否发生了变化1
2
3target.a = 3
console.log(target) // { a: 3, b: 2, c: { d: 'hello', e: 'world' } }
console.log(obj) // { a: 1, b: 2, c: { d: 'hello', e: 'world' } }我们对target中的属性a做了改变,然后打印obj。我们发现obj中的属性a并没有发生变化,这是因为属性a的值是基本数据类型,下面我们再来改变一下target中的属性c的e属性。看看会发生什么变化.
1
2
3target.c.e = 'javascript'
console.log(target) // { a: 1, b: 2, c: { d: 'hello', e: 'javascript' } }
console.log(obj) // { a: 1, b: 2, c: { d: 'hello', e: 'javascript' } }- 首先实现以下浅拷贝
根据上面的打印结果我们发现,当修改一个浅拷贝出来的对象属性(该属性的值是一个引用类型)时,原来的对象同时也被改变了,这是因为javascript中的引用类型是存在内存中的堆中(访问是通过引用访问,内存地址),而基本数据类型是存储在栈中。下面我们来对上面的浅拷贝函数进行一些修改1
2
3
4
5
6
7
8
9
10
11
12
13function deepCopy(obj){
let target = {}
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (typeof obj[key] === 'object') {
target[key] = deepCopy(obj[key])
} else {
target[key] = obj[key]
}
}
}
return target
}
测试一下我们修改过后的深拷贝函数1
2
3
4var target1 = deepCopy(obj)
target1.c.e = 'javascript'
console.log(target1) // { a: 1, b: 2, c: { d: 'hello', e: 'javascript' } }
console.log(obj) // { a: 1, b: 2, c: { d: 'hello', e: 'world' } }
从打印的结果我们可以看出,target1
中的引用对象被修改了值,但是obj
中的值并没有被修改,所以我们实现了一个基本的深拷贝。下面我们看看这个深拷贝的问题:
如果传入的是参数是null或者是undefined的情况下,我们返回的是什么?
1
2var target2 = deepCopy(null) // deepCopy(undefined)
console.log(target2) // {}当我们传如的参数是
null或者undefined
的情况下,深拷贝出来的是一个{}
,这不符合我们的预期,我们期望打印出来的是null或者是undefined
。所以我们需要对传入的参数做验证.我们可以通过对象的toString方法来判断对象的类型,但是我们需要保留数组的情况,所以仍然使用typeof来判断1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function isObject(obj){
return typeof obj === 'object' && obj ! = null
}
function deepCopy(obj){
if (!isObject(obj)) return obj; // 对传入的参数做数据类型验证
let target = {}
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (isObject(obj[key])) {
target[key] = deepCopy(obj[key])
} else {
target[key] = obj[key]
}
}
}
return target
}
测试一下改进之后的深拷贝1
2
3
4
5
6var target = deepCopy(null)
console.log(target) // null
var target = deepCopy(undefined)
console.log(target) // undefined
var target = deepCopy(obj)
console.log(target) // { a: 1, b: 2, c: { d: 'hello', e: 'world' }, fn: [Function: fn] }
改进之后的深拷贝基本已经可以将null和undefined给过滤掉了。下面,我们考虑一下数组的情况1
2
3
4
5
6
7
8
9
10
11
12
13
14function deepCopy(obj){
if (!isObject(obj)) return obj
let target = Array.isArray(obj) ? [] : {}
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (isObject(obj[key])){
target[key] = deepCopy(obj[key])
} else {
target[key] = obj[key]
}
}
}
return target
}
当使用JSON序列化来处理循环引用的时候会报错,导致拷贝失败,所以我们在自己实现深拷贝的时候也需要考虑循环引用的问题。解决方案就是使用hash来存储已经拷贝过的对象,当检测到当前的对象已经存在于hash中的时候,直接返回hash中的对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function deepCopy(obj, hash = new WeakMap()) {
if (!isObject(obj)) return obj
if (hash.has(obj)) return hash.get(obj)
let target = Array.isArray(obj) ? [] : {}
hash.set(obj, target)
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (isObject(obj[key])){
target[key] = deepCopy(obj[key], hash)
} else {
target[key] = obj[key]
}
}
}
return target
}
// 测试一下代码
let object = {a:1}
object.b = object
let target = deepCopy(object)
console.log(target) // { a: 1, b: [Circular] }