JavaScript深浅拷贝方法总结

211 阅读3分钟

引言

在探讨这个问题之前我们现来看下JavaScript当中的数据类型:

  • 基本数据类型:String, Number, Boolean, Null, Undefined,Symbol

  • 对象数据类型:数组(Array)、函数(Function)特殊对象:正则(RegExp)和日期(Date)

两种数据类型当中最大的区别是储存的位置,基本数据类型直接存储在栈(stack)中,引用数据类型在栈中存储的是该对象在栈中引用,真实的数据存放在堆内存里

我们拷贝对象数据类型的时候,拷贝的是栈内存上的地址指针。因此复制之后不同实例实际上指向同一个堆内存空间,改动时会相互影响。由此产生了深浅拷贝的区别,即这个问题只存在于对象数据类型当中

let p = {
 name: 'Tyrone',
 prop: {
  age: [1, 2, 3],
 },
};

let a = p;
a.name = 'Bob';
console.log(JSON.stringify(a));
// {"name":"Bob","prop":{"age":[1,2,3]},"age":2}

console.log(JSON.stringify(p));
// {"name":"Bob","prop":{"age":[1,2,3]},"age":2}

在长篇大论之前,先摆上总结的方案:

浅拷贝实现方案

  • Object.assign
  • 展开运算符...
  • 函数实现

深拷贝实现方案

  • JSON.parse(JSON.stringify(object))
  • 递归函数实现
  • 第三方函数库

一、浅拷贝

浅拷贝原理是新建一个对象,然后将原始对象的属性精确拷贝给新的对象。如果原始对象的属性为基本类型的值,就会拷贝具体数值,如果是引用类型,则拷贝这个内存地址。所以浅拷贝实际上是实现了第一层的拷贝,但是第二层的变动依然会相互影响。

1. Object.assign

MDN解释:

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

Object.assign(target, ...sources)

Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标对象的[[Set]],所以它会调用相关 getter 和 setter。

let a = Object.assign({}, p);
a.name = 'Bob';
a.prop.age.pop();
console.log(JSON.stringify(a)); 
// {"name":"Bob","prop":{"age":[1,2]}}

console.log(JSON.stringify(p)); 
// {"name":"Tyrone","prop":{"age":[1,2]}}

2. 展开运算符

let a = { ...p };

a.name = 'Bob';
a.prop.age.pop();
console.log(JSON.stringify(a)); 
// {"name":"Bob","prop":{"age":[1,2]}}

console.log(JSON.stringify(p));
// {"name":"Tyrone","prop":{"age":[1,2]}}

3. 函数实现

// 浅拷贝
function extendCopy(p) {
 let res = {};
 for (const i in p) {
  res[i] = p[i];
 }
 return res;
}

let a = extendCopy(p);

a.name = 'Bob';
a.prop.age.pop();
console.log(JSON.stringify(a)); 
// {"name":"Bob","prop":{"age":[1,2]}}

console.log(JSON.stringify(p)); 
// {"name":"Tyrone","prop":{"age":[1,2]}}

二、深拷贝

在上面的前拷贝当中,如果第一层存在引用类型的数据的话,又回到了最开始的问题。因此,我们需要设计方案将每一层的引用类型实现拷贝:

1. JSON.parse(JSON.stringify(object))

原理JSON.stringify()将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,在这个过程当中,产生的新对象回生成新的储存空间,实现深拷贝。

let a = JSON.parse(JSON.stringify(p));

a.name = 'Bob';
a.prop.age.pop();
console.log(JSON.stringify(a)); 
// {"name":"Bob","prop":{"age":[1,2]}}

console.log(JSON.stringify(p)); 
// {"name":"Tyrone","prop":{"age":[1,2,3]}}

该方法也是有局限性的

  • 会忽略 undefined
  • 不能序列化函数(将对象转化为JSON字符串,不接受函数)
  • 不能解决循环引用的对象

2. 函数实现

通过递归的方式实现深度拷贝:通过判断引用类型新建相应的数据,依次递归直到所有数据都为基本类型。

// 深拷贝
function deepCopy(p, res = {}) {
 for (const i in p) {
  if (typeof p[i] === 'object') {
   res[i] = p[i].constructor === Array ? [] : {};
   deepCopy(p[i], res[i]);
  } else {
   res[i] = p[i];
  }
 }
 return res;
}
let a = deepCopy(p);

a.name = 'Bob';
a.prop.age.pop();
console.log(JSON.stringify(a)); 
// {"name":"Bob","prop":{"age":[1,2]}}

console.log(JSON.stringify(p));
// {"name":"Tyrone","prop":{"age":[1,2,3]}}

3. 函数库

可以通过lodash等函数库实现深度拷贝。


感谢您的阅读!此文章已收录我的公众号和博客站点!欢迎关注!