详解JS深拷贝与浅拷贝:原理、实现与局限性

作者:纸飞机

时间:2026-10-23

浏览:414

本文深入解析JavaScript中深拷贝与浅拷贝的区别。通过基本类型与引用类型的内存机制,阐述Object.assign、slice、concat等浅拷贝方法的局限。重点介绍JSON.parse/stringify深拷贝的缺陷,并提供支持原型链、循环引用及正则日期对象的手写深拷贝函数实现,帮助开发者选择正确的数据复制策略。

文章目录

预备知识:JS数据类型与复制机制

预备知识

1.1、JS数据类型

JavaScript的数据类型主要分为两类:基本数据类型,包括Boolean、String、Number、null和undefined;以及引用数据类型,涵盖Object、Array、Function、RegExp、Date等复杂类型。

1.2、数据类型的复制

基本数据类型的复制遵循按值传递的原则,即复制的是具体的数值本身。


var a = 1;
var b = a;
b = 2;
console.log(a); // 1
console.lob(b); // 2

引用数据类型的复制则是按引用传值,即复制的是指向内存中对象的地址指针。


var obj1 = {
 a: 1;
 b: 2;
};
var obj2 = obj1;
obj2.a=3;
console.log(obj1.a); //3
console.log(obj2.a); // 3

1.3、深拷贝与浅拷贝

深拷贝和浅拷贝的概念仅适用于引用数据类型。浅拷贝会逐个拷贝对象的成员,但仅复制内存地址,新旧对象的成员仍共享同一块内存空间;深拷贝则会创建一个完全独立的新对象,新对象与原对象不共享内存,修改新对象不会影响原对象。

两者的核心区别在于:浅拷贝仅复制对象的第一层属性,而深拷贝会对对象属性进行递归复制,确保所有层级的引用类型都被独立创建。

JS浅拷贝:原理与常见实现

二、JS浅拷贝

2.1、赋值与浅拷贝

当将一个对象赋值给新变量时,实际上是将栈内存中的地址赋给了新变量,而非堆内存中的数据。这意味着新旧变量指向同一个存储空间,对任一变量的修改都会反映在另一个变量上,导致两者联动变化。


var obj1 = {
  'name' : 'zhangsan',
  'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

预备知识:JS数据类型与复制机制

浅拷贝是按位拷贝对象,它会创建一个新对象并依次拷贝原有对象的成员。若成员为基本类型,则拷贝其值;若为引用类型,则拷贝其内存地址。因此,若新对象中某个引用类型的成员地址发生变化(如重新赋值),不会影响原对象;但若修改该引用类型内部的属性,则会同时影响原对象。


//手写浅拷贝
function shallowCopy(obj1) {
 let obj2 = Array.isArray(obj1) ? [] : {}
 for (let i in obj1) {
  obj2[i] = obj1[i]
 }
 return obj2
}
var obj1 = {
  'name' : 'zhangsan',
  'language' : [1,[2,3],[4,5]],
};
var obj2 = shallowCopy(obj1);
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

预备知识:JS数据类型与复制机制

2.2、浅拷贝的实现

(1)Object.assign()

Object.assign()方法可将源对象自身的可枚举属性拷贝到目标对象,并返回目标对象。该方法执行的是浅拷贝,即拷贝属性的引用而非对象本身。此方法适用于Array和Object类型。


var obj1 = {
  'name' : 'zhangsan',
  'language' : [1,[2,3],[4,5]],
};
var obj2 = Object.assign({}, obj1);
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

预备知识:JS数据类型与复制机制

(2)Array.prototype.concat()和Array.prototype.slice()

Array.prototype.concat()和Array.prototype.slice()均为Array原型上的方法,仅适用于数组类型。


var arr1 = [1,3,{
 user: 'aaa'
}]
var arr2 = arr1.concat();
arr2[0] = '一';
arr2[2].user = 'AAA';
console.log('arr1',arr1)
console.log('arr2',arr2)


var arr1 = [1,3,{
 user: 'aaa'
}]
var arr2 = arr1.slice();
arr2[0] = '一';
arr2[2].user = 'AAA';
console.log('arr1',arr1)
console.log('arr2',arr2)

预备知识:JS数据类型与复制机制

补充说明:Array的slice和concat方法均不会修改原数组,而是返回一个经过浅拷贝的新数组。与Object.assign()类似,它们仅对第一层属性进行拷贝:若第一层属性为基本数据类型,则拷贝值;若为引用数据类型,则拷贝内存地址。

JS深拷贝:JSON方法与手写实现

三、JS深拷贝

深拷贝要求遍历对象中所有引用类型的值,直到遇到基本类型的值为止。

3.1、深拷贝实现方式

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

原理:利用JSON.stringify()将对象转换为JSON字符串,再通过JSON.parse()将字符串解析回对象。


var obj1 = {
  'name' : 'zhangsan',
  'language' : [1,[2,3],[4,5]],
};
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

预备知识:JS数据类型与复制机制

缺点:该方法虽能实现数组、对象和基本数据类型的深拷贝,但无法处理函数,因为JSON.stringify()不接受函数。此外还存在以下局限性:

  • 若对象中包含时间对象,拷贝后的时间将变为字符串形式,而非时间对象。
  • 若对象中包含RegExp或Error对象,序列化结果将为空。
  • 若对象中包含函数或undefined,序列化结果会丢失这些值。
  • 若对象中包含NaN、Infinity或-Infinity,序列化结果将变为null。
  • JSON.stringify()仅序列化对象的可枚举自有属性。若对象由构造函数生成,拷贝后会丢失constructor属性。
  • 若对象中存在循环引用,该方法无法正确实现深拷贝。

(2)手写深拷贝函数

通过递归实现深拷贝。


function deepCopy(obj){
 var result= Array.isArray(obj) ? [] : {}
 if (obj && typeof(obj) === 'object') {
  for (let i in obj) {
   if (obj.hasOwnProperty(i)){ // 思考:这句是否有必要?
    if (obj[i] && typeof(obj[i]) === 'object') {
     result[i] = deepCopy(obj[i])
    } else {
     result[i] = obj[i]
    }
   }
  }
 }
 return result
}
var obj1 = {
 a: 1,
 b: {
  c: 2
 }
};
var obj2 = deepCopy(obj1);
obj2.a = '一';
obj2.b.c = '二'
console.log('obj1', obj1)
console.log('obj2', obj2)

obj.hasOwnProperty(prop)用于判断对象obj是否包含属性prop,返回布尔值,存在则返回true,否则返回false。

上述简单实现存在缺陷:当遇到两个互相引用的对象时,会导致死循环从而引发栈溢出。为避免此问题,需在遍历时检测是否存在相互引用。

深拷贝函数改进(防止循环递归爆栈)


function deepCopy(obj, parent = null) {
 let result = Array.isArray(obj) ? [] : {}
 let _parent = parent
 // 该字段有父级则需要追溯该字段的父级
 while(_parent) {
  // 如果该字段引用了它的父级,则为循环引用
  if (_parent.originalParent === obj) {
   // 循环引用返回同级的新对象
   return _parent.currentParent 
  }
  _parent = _parent.parent
 }
 if (obj && typeof(obj) === 'object') {
  for (let i in obj) {
   // 如果字段的值也是一个对象
   if (obj[i] && typeof(obj[i]) === 'object') {
    // 递归执行深拷,将同级的待拷贝对象传递给parent,方便追溯循环引用
    result[i] = deepCopy(obj[i], {
     originalParent: obj,
     currentParent: result,
     parent: parent
    })
   } else {
    result[i] = obj[i]
   }
  }
 }
 return result
}
var obj1 = {
 x: 1,
 y: 2
};
obj1.z = obj1
var obj2 = deepCopy(obj1)
console.log('obj1', obj1)
console.log('obj2', obj2)

以上代码可直接复制到浏览器中测试运行。

深拷贝函数最终版(支持基本数据类型、Array、Object、原型链、RegExp、Date类型)


function deepCopy(obj, parent = null) {
 let result
 let _parent = parent
 while(_parent) {
  if (_parent.originalParent === obj) {
   return _parent.currentParent
  }
  _parent = _parent.parent
 }
 if (obj && typeof(obj) === 'object') {
  if (obj instanceof RegExp) {
   result = new RegExp(obj.source, obj.flags)
  } else if (obj instanceof Date) {
   result = new Date(obj.getTime())
  } else {
   if (obj instanceof Array) {
    result = []
   } else {
    let proto = Object.getPrototypeOf(obj)
    result = Object.create(proto)
   }
   for (let i in obj) {
    if(obj[i] && typeof(obj[i]) === 'object') {
     result[i] = deepCopy(obj[i], {
      originalParent: obj,
      currentParent: result,
      parent: parent
     })
    } else {
     result[i] = obj[i]
    }
   }
  }
 } else {
  return obj
 }
 return result
}
var obj1 = {
 x: 1 
}

//试调用
function construct(){
  this.a = 1,
  this.b = {
    x:2,
    y:3,
    z:[4,5,[6]]
  },
  this.c = [7,8,[9,10]],
  this.d = new Date(),
  this.e = /abc/ig,
  this.f = function(a,b){
    return a+b
  },
  this.g = null,
  this.h = undefined,
  this.i = "hello",
  this.j = Symbol("foo")
}
construct.prototype.str = "I'm prototype"
var obj1 = new construct()
obj1.k = obj1
obj2 = deepCopy(obj1)

obj2.b.x = 999
obj2.c[0] = 666

console.log('obj1', obj1)
console.log('obj2', obj2)

(3)函数库

也可使用第三方函数库,例如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

参考

https://www.jb51.net/article/181898.htm

https://www.jb51.net/article/140928.htm

以上就是详解JS深拷贝与浅拷贝的详细内容,更多关于JS深拷贝与浅拷贝的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章