【JS专栏】JS对象的浅拷贝与深拷贝

简介: 【JS专栏】JS对象的浅拷贝与深拷贝

浅拷贝


自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象。


1. object.assign


object.assign 是 ES6 中 object 的一个方法,该方法可以用于JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝


该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。


object.assign 的语法为:Object.assign(target, ...sources)


const target = {};
const source = { a: { b: 1 } };
Object.assign(target, source);
console.log(target); // { a: { b: 1 } };


但是使用 object.assign 方法有几点需要注意:


  • 它不会拷贝对象的继承属性;
  • 它不会拷贝对象的不可枚举的属性;
  • 可以拷贝 Symbol 类型的属性。


const obj1 = {
  a: {
    b: 1
  },
  c: 1,
  sym: Symbol(1)
};
Object.defineProperty(obj1, 'innumerable', {
  value: '1',
  enumerable: false
});
const obj2 = {};
Object.assign(obj2, obj1)
obj1.a.b = 2;
obj1.c = 2;
console.log('obj1', obj1); // {a: {b: 2}, c: 2, sym: Symbol(1), innumerable: "1"}
console.log('obj2', obj2); // {a: {b: 2}, c: 1, sym: Symbol(1)}


从上面的样例代码中可以看到,利用 object.assign 也可以拷贝 Symbol 类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能。


2. 扩展运算符方式


扩展运算符的语法为:let cloneObj = { ...obj };


/* 对象的拷贝 */
const obj = {
  a: 1,
  b: {
    c: 1
  }
}
const obj2 = {
  ...obj
}
obj.a = 2
console.log(obj) //{a:2,b:{c:1}} 
console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj) //{a:2,b:{c:2}} 
console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果


扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。


3. concat 拷贝数组


数组的 concat 方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过 concat 只能用于数组的浅拷贝,使用场景比较局限。


const arr = [1, 2, 3, { a: 1 }];
const newArr = arr.concat();
newArr[1] = 0;
console.log(arr);  // [ 1, 2, 3, {a: 1} ]
console.log(newArr); // [ 1, 0, 3, { a: 1 } ]
newArr[3].a = 4;
console.log(arr); // [ 1, 2, 3, {a: 4} ]
console.log(newArr); // [ 1, 2, 3, {a: 4} ]


4. slice 拷贝数组


slice 方法也比较有局限性,因为它仅仅针对数组类型。slice 方法会返回一个新的数组对象,这一对象由该方法的前两个参数来决定原数组截取的开始和结束位置,是不会影响和改变原始数组的。但是,数组元素是引用类型的话,也会影响到原始数组。


slice 的语法为:arr.slice(begin, end);


const arr = [1, 2, { val: 4 }];
const newArr = arr.slice();
newArr[2].val = 5;
newArr[1] = 3;
console.log(arr);  //[ 1, 2, { val: 5 } ]


从上面我们可以看出,这就是浅拷贝的限制所在——它只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝将无能为力。因此深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝


我们总结一下浅拷贝的原理:


  1. 对基础类型做一个最基本的一个拷贝;
  2. 对引用类型开辟一个新的存储,并且拷贝一层对象属性。


以下是对浅拷贝的简单实现:


const shallowClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}


深拷贝


浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。


深拷贝后的对象与原始对象是相互独立、不受影响的,彻底实现了内存上的分离


总的来说,深拷贝的原理可以总结如下:


将原对象从内存中完整地拷贝出来一份给新对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。


1. JSON.stringify


把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象。


const obj1 = { a: 1, b: [1, 2, 3] }
const str = JSON.stringify(obj1);
const obj2 = JSON.parse(str);
console.log(obj2);   //{a:1,b:[1,2,3]} 
obj1.a = 2;
obj1.b.push(4);
console.log(obj1);   //{a:2,b:[1,2,3,4]}
console.log(obj2);   //{a:1,b:[1,2,3]}


可以看到通过改变 obj1 的 b 属性,其实可以看出 obj2 这个对象也不受影响。


但是,JSON.stringify并不是那么完美的,它也有局限性。


  • 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
  • 拷贝 Date 引用类型会变成字符串;
  • 无法拷贝不可枚举的属性;
  • 无法拷贝对象的原型链;
  • 拷贝 RegExp 引用类型会变成空对象;
  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
  • 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。


我们通过代码来看下效果。


let obj = {
  func: function () { alert(1) },
  obj: { a: 1 },
  arr: [1, 2, 3],
  und: undefined,
  reg: /123/,
  date: new Date(0),
  NaN: NaN,
  infinity: Infinity,
  sym: Symbol('1')
}
Object.defineProperty(obj, 'innumerable', {
  enumerable: false,
  value: 'innumerable'
});
console.log('obj', obj); // { NaN: NaN , arr: (3) [1, 2, 3] ,date: Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间) {}, func: ƒ(), infinity: Infinity , obj: { a: 1 } , reg: /123/, sym: Symbol(1), und: undefined, innumerable: "innumerable" }
const str = JSON.stringify(obj);
const obj1 = JSON.parse(str);
console.log('obj1', obj1); // { NaN: null, arr: (3) [1, 2, 3], date: "1970-01-01T00:00:00.000Z", infinity: null, obj: {a: 1}, reg: {} }


2. 实现深拷贝方法


下面是一个实现 deepClone 函数封装的例子,有几点需要注意下。


  • WeakMap 是弱引用类型,可以有效防止内存泄漏;
  • 能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;
  • 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性;
  • 结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链;
  • 当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;


const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
  if (obj.constructor === Date) 
  return new Date(obj)       // 日期对象直接返回一个新的日期对象
  if (obj.constructor === RegExp)
  return new RegExp(obj)     //正则对象直接返回一个新的正则对象
  //如果循环引用了就用 weakMap 来解决
  if (hash.has(obj)) return hash.get(obj)
  let allDesc = Object.getOwnPropertyDescriptors(obj)
  //遍历传入参数所有键的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
  //继承原型链
  hash.set(obj, cloneObj)
  for (let key of Reflect.ownKeys(obj)) { 
    cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
  }
  return cloneObj
}
// 下面是验证代码
let obj = {
  num: 0,
  str: '',
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: '我是一个对象', id: 1 },
  arr: [0, 1, 2],
  func: function () { console.log('我是一个函数') },
  date: new Date(0),
  reg: new RegExp('/我是一个正则/ig'),
  [Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
  enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj    // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)



相关文章
|
2月前
|
JavaScript 前端开发
JavaScript遍历数组和对象常用方法总结
以上代码展示了数组和对象的多种遍历方法。对于数组,使用了传统的 `for` 循环、`for...in` 和 ES6 的 `for...of` 进行遍历;对于对象,则通过 `for...in`、`Object.keys()`、`Object.values()` 和 `Object.entries()` 来获取键值对。`for...of` 循环适用于遍历具有迭代协议的数据结构,如数组、字符串等,而对象遍历则更多地依赖于 `Object` 方法来获取其属性集合。
JavaScript遍历数组和对象常用方法总结
|
2月前
|
JavaScript 前端开发 索引
JS遍历数组里数组下的对象,根据数组中对象的某些值,组合成新的数组对象
这篇文章介绍了如何在JavaScript中遍历数组里数组下的对象,并根据对象的某些属性值组合成一个新的数组对象。主要内容包括使用ES6的`for...of`循环来遍历数组对象,然后根据需要提取对象中的属性值,并将它们放入新的对象中,最终形成一个新的对象数组以供使用。
|
2天前
|
JSON JavaScript 前端开发
js如何格式化一个JSON对象?
js如何格式化一个JSON对象?
10 3
|
9天前
|
JavaScript 前端开发
js之浏览器对象|28
js之浏览器对象|28
|
10天前
|
JSON JavaScript 数据格式
手写JS实现深拷贝函数
本文介绍了如何实现一个深拷贝函数`deepClone`,该函数可以处理对象和数组的深拷贝,确保拷贝后的对象与原始对象在内存中互不干扰。通过递归处理对象的键值对和数组的元素,实现了深度复制,同时保留了函数类型的值和基础类型的值。
15 3
|
1月前
|
JavaScript 前端开发
JavaScript基础知识-枚举对象中的属性
关于JavaScript基础知识中如何枚举对象属性的介绍。
27 1
JavaScript基础知识-枚举对象中的属性
|
23天前
|
JavaScript 前端开发
JavaScript Boolean(布尔) 对象
Boolean(布尔)对象用于将非布尔值转换为布尔值(true 或者 false)。
29 8
|
5天前
|
存储 JavaScript 前端开发
JavaScript Number 对象
JavaScript Number 对象
9 0
|
5天前
|
JavaScript 前端开发
JavaScript prototype(原型对象)
JavaScript prototype(原型对象)
11 0
|
5天前
|
JavaScript 前端开发
JavaScript 对象
JavaScript 对象
10 0