阅读 307
JavaScript 深浅拷贝

JavaScript 深浅拷贝

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

浅拷贝

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

Object.assign

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

Object.assign(target, ...sources)

const target = { a: 1, b: 2 }
const source = { b: 4, c: 5 }
const returnedTarget = Object.assign(target, source)

console.log(target)           // Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget)   // Object { a: 1, b: 4, c: 5 }
复制代码

使用 Object.assign 需要注意以下几点:

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

Object.assign 循环遍历原对象的属性,通过复制的方式将其赋值给目标对象的相应属性

扩展运算符

可以使用扩展运算符,在构造对象的同时完成浅拷贝。

let cloneObj = { ...obj };

/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let 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]  // newArr [1,2,3]
复制代码

concat 拷贝数组

var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])

let arr = [1, 2, 3]
let newArr = arr.concat()
newArr[1] = 100
console.log(arr)      // [ 1, 2, 3 ]
console.log(newArr)   // [ 1, 100, 3 ]
复制代码

slice 拷贝数组

arr.slice([begin[, end]])

let arr = [1, 2, {val: 4}]
let newArr = arr.slice()
newArr[2].val = 1000
console.log(arr)     // [ 1, 2, { val: 1000 } ]
复制代码

深拷贝

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

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

JSON.stringify()

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

let obj1 = { a:1, b:[1,2,3] }
let str = JSON.stringify(obj1)
let 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]}
复制代码

使用 JSON.stringify() 需要注意

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

递归实现

// 判断是否复杂类型
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)
  
  // 使用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性
  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)
复制代码

image.png

最后说一句

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力,多谢支持。

文章分类
前端
文章标签