JavaScript之深拷贝和浅拷贝

50 阅读4分钟

在谈深拷贝和浅拷贝之前,我们需要了解JavaScript有哪些数据类型。

JavaScript有六种基本数据类型:

  • String(字符串)
  • Number(数字)
  • Boolean(布尔)
  • Null(空)
  • Undefined(未定义)
  • Symbol(唯一)

注意,简单基本类型(string、boolean、number、null 和 undefined)本身并不是对象。 null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行 typeof null 时会返回字符串 "object"。实际上,null 本身是基本类型。

Symbol是ES6引入的一种新的基本数据类型,表示独一无二的值。

常见引用数据类型:

  • Object(对象)
  • Function(函数)
  • Date(日期)
  • RegExp(正则)
  • Error(错误)
  • Array(数组)

JavaScript内置对象:

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

1.浅拷贝

浅拷贝:对于基本数据类型来说,浅拷贝拷贝的是值,对于拷贝的是地址

例如:

// 基本数据类型示例
let a = 1;
let b = a;
a = 2;
console.log(b) // 1

//引用数据类型示例
let obj = {
    a: 1,
    b: 2,
};
let obj1 = obj;
obj.a = 3;
console.log(obj1); // { a: 3, b: 2 }
obj1.c = 4;
console.log(obj) // { a: 3, b: 2, c: 4 }

// 很明显,当拷贝是的基本数据类型时,浅拷贝就为值拷贝。
// 当拷贝的是引用数据类型时,浅拷贝就相当于拷贝的是地址。
// 无论是被拷贝的数据改变或者是拷贝得到的数据改变都会引起另一方的改变

2.深拷贝

深拷贝:对于引用数据类型来说,深拷贝就是开辟一个新的地址来存储值

深拷贝的方法和注意事项:

2.1 JSON深拷贝

let obj = {
    a: 1,
    b: 2,
};
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj1); // { a: 1, b: 2 }
obj.c = 3;
console.log(obj1); // { a: 1, b: 2 }
obj1.d = 2;
console.log(obj); // { a: 1, b: 2, c: 3 }

// 这里可以很明显的看到当obj改变时不会引起obj1的改变,当obj1改变时也不会引起obj改变,这就完成了深拷贝

那JSON.parse(JSON.stringify(Object))就可以了,我还需要知道其他深拷贝方法干嘛呢?

我们看代码:

let obj = {
    a: 1,
    b: 2,
};
obj.c = obj;
let obj1 = JSON.parse(JSON.stringify(obj));

image.png 出错了?因为JSON.stringify不能对一个循环引用的对象转换为字符串

2.2 Object.assign

let obj = {
    a: 1,
    b: 2,
};
let obj1 = Object.assign({}, obj);
console.log(obj1); // { a: 1, b: 2 }
obj.c = 3;
console.log(obj1); // { a: 1, b: 2 }
obj1.d = 2;
console.log(obj); // { a: 1, b: 2, c: 3 }

那Object.assign是不是也可以实现深拷贝了呢?

我们继续看代码:

let obj = {
    a: 1,
    b: 2,
    c: {
        str: "111",
    },
};
let obj1 = Object.assign({}, obj);
console.log(obj1); // { a: 1, b: 2, c: { str: '111' } }
obj.c.str = "222";
console.log(obj1); // { a: 1, b: 2, c: { str: '222' } }
obj1.c.num = 2;
console.log(obj); // { a: 1, b: 2, c: { str: '222', num: 2 } }
// 我们可以看到,当obj的某个属性值为引用数据类型时,该属性值的改变会引起obj1的改变,同样,obj1的改变也会引起obj的改变

2.3 Object.defineProperty

let obj = {
    a: 1,
    b: 2,
};
let obj1 = {};
for (let key in obj) {
    Object.defineProperty(obj1, key, {
        value: obj[key],
    });
}
console.log(obj1); // { a: 1, b: 2 }
obj.c = 3;
console.log(obj1); // { a: 1, b: 2 }
obj1.d = 2;
console.log(obj); // { a: 1, b: 2, c: 3 }
let obj = {
	a: 1,
	b: 2,
	c: {
		str: "111",
	},
};
let obj1 = {};
for (let key in obj) {
	Object.defineProperty(obj1, key, {
		value: obj[key],
	});
}
console.log(obj1); // { a: 1, b: 2, c: { str: '111' } }
obj.c.str = "222";
console.log(obj1); // { a: 1, b: 2, c: { str: '222' } }
obj1.c.num = 2;
console.log(obj); // { a: 1, b: 2, c: { str: '222', num: 2 } }

可以看到,当obj的某个属性值为引用数据类型时Object.defineProperty和Object.assign一样,无法完全实现深拷贝。

2.4 递归

简易版:

function deepClone(obj) {
    let newObj = {};
    for (let key in obj) {
        // 如果是引用数据类型,就递归深拷贝
        if (typeof obj[key] === "object") {
            newObj[key] = deepClone(obj[key]);
        } else {
            newObj[key] = obj[key];
        }
    }
    return newObj;
}

升级版:(考虑null和array)

function deepClone(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return obj;
  }
  const target = Array.isArray(obj) ? [] : {};
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      if (typeof obj[key] === 'object' && obj[key] !== null) {
        target[key] = deepClone(obj[key]);
      } else {
        target[key] = obj[key];
      }
    }
  }
  return target;
}

终极版:(考虑symbol)

const deepClone = (obj, cache) => {
  if(!cache){
    cache = new Map() 
  }
  if(obj instanceof Object) { // 不考虑跨 iframe
    if(cache.get(obj)) { return cache.get(obj) }
    let result 
    if(obj instanceof Function) {
      if(obj.prototype) { // 有 prototype 就是普通函数
        result = function(){ return obj.apply(this, arguments) }
      } else {
        result = (...args) => { return obj.call(undefined, ...args) }
      }
    } else if(obj instanceof Array) {
      result = []
    } else if(obj instanceof Date) {
      result = new Date(obj - 0)
    } else if(obj instanceof RegExp) {
      result = new RegExp(obj.obj, obj.flags)
    } else {
      result = {}
    }
    cache.set(obj, result)
    for(let key in obj) { 
      if(obj.hasOwnProperty(key)){
        result[key] = deepClone(obj[key], cache) 
      }
    }
    return result
  } else {
    return obj
  }
}

2.5引用第三方库如loadsh

这里就不做详细介绍了。

总结

  1. 对于基本数据类型,浅拷贝就是对值的拷贝,对于引用数据类型,浅拷贝只引用了其地址。
  2. 关于深拷贝:对于非循环引用的数据,JSON.parse(JSON.stringify(Object))就可以实现深拷贝,而循环引用的数据类型,JSON.parse(JSON.stringify(Object))就会因为JSON.stringify不能对循环引用数据转换而抛出异常。
  3. Object.assign和Object.defineProperty实际上还是浅拷贝,实际上就相当于遍历第一层属性值对其赋值。

注:Object.defineProperty还可以对属性设置一些特性。