• 使用JSON.stringify()和JSON.parse()实现简单的深拷贝

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var 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. 拷贝的对象中存在函数时,拷贝失败
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      var 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. 循环引用会导致拷贝失败
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      var 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
    7
    var a = Symbol()
    var obj = {
    a:a,
    b:undefined
    }
    var obj1 = JSON.parse(JSON.stringify(obj))
    console.log(obj1) // {}

    我们声明了一个变量a为Symbole。然后将其赋值给obj对象的a属性,给obj对象添加了一个属性值为undefinedb属性,然后使用JSON.stringify进行序列化,发现打印出来的是一个{}对象,说明序列化失败.
    4:在数组中,不能被序列化的元素会被使用null进行填充

    1
    2
    3
    const 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
    11
    class 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
    12
    JSON.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. 首先实现以下浅拷贝
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      function 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
    3
    target.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
    3
    target.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
13
function 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
4
var 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中的值并没有被修改,所以我们实现了一个基本的深拷贝。下面我们看看这个深拷贝的问题:

  1. 如果传入的是参数是null或者是undefined的情况下,我们返回的是什么?

    1
    2
    var 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
    17
    function 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
6
var 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
14
function 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
22
function 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] }