简单了解深拷贝与浅拷贝

235 阅读4分钟

这是我参与新手入门的第1篇文章

概念

首先要提一下JavaScript中的数据类型。在JavaScript中一共有8种数据类型: StringNumberBooleanNullUndefinedObjectSymbolbigInt

Number支持的整数范围有限,会自动四舍五入太大的整数,导致失去精度,bigInt支持比Number更大的整数,适用于数学运算。

除了Object为引用数据类型,其他7种皆为基本数据类型,基本数据类型之间复制返回的是新的数据,都是在独立的栈中,可随意复制不受影响。

引用数据类型Object,像ArrayFunctionDateRegExp...都属于引用数据类型

引用数据类型存放在堆内存中,可以直接访问和修改,但由于引用数据类型占据空间大,放在栈中存在性能问题,所以在栈中保存了一份指针用来指向堆中的原始地址,当解释器寻找引用值时,首先检索栈中的地址,通过地址从堆中获取数据

引用型数据类型的赋值是从栈中复制了一份地址指针,所以两个变量指向的是同一个对象,修改任意一个变量都会导致另一个的改变,有时候需要复制一份新的独立的数据,这时就需要一些方法进行拷贝。

指针指向同一个地址:

let obj = {
  name:'张三',
  age:20,
}

let newObj = obj     // 直接赋值,只赋值了引用的指针
obj.name = '李四'    // 修改obj中属性,newObj中的也会变,因为它俩本质上是同一个

console.log(obj)     // { name: '李四', age: 20 }
console.log(newObj)  // { name: '李四', age: 20 }

浅拷贝

浅拷贝只是拷贝一层,更深层次对象级别的只拷贝引用(地址),有如下几种实现方法:

循环

通过循环遍历赋值可实现浅拷贝

let obj = {
  name:'张三',
  age:20,
  skill: {
    name: 'javascript'   
  }
}

let newObj = {}

for (let key in obj) {
  newObj[key] = obj[key];
}

obj.name = '李四'
obj.skill = 'Node'

console.log(obj)    // { name: '李四', age: 20, skill: { name: 'Node' } }
console.log(newObj) // { name: '张三', age: 20, skill: { name: 'Node' } }

Object.assign

ES6对象新增的方法 详见

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

语法:

Object.assign(target, ...sources)

参数

target 目标对象

sources 源对象

返回

源对象

例子

let obj = {
  name:'张三',
  age:20,
  skill: {
    name: 'javascript'   
  }
}

let newObj = Object.assign({}, obj)

obj.name = '李四'    // 第一层拷贝值
obj.skill = 'Node'  // 第二层拷贝引用

// skill 还是引用的同一个
console.log(obj)    // { name: '李四', age: 20, skill: { name: 'Node' } }
console.log(newObj) // { name: '张三', age: 20, skill: { name: 'Node' } }

扩展运算符(...

扩展运算符为ES6新增特性 详见

let obj = {
  name:'张三',
  age:20,
  skill: {
    name: 'javascript'   
  }
}

let newObj = { ...obj}

obj.name = '李四'  
obj.skill = 'Node' 

// 同样只拷贝第一层
console.log(obj)    // { name: '李四', age: 20, skill: { name: 'Node' } }
console.log(newObj) // { name: '张三', age: 20, skill: { name: 'Node' } }

缺陷

如果拷贝对象中有引用型值的话,引用型值是指向同一个地址的,修改一个会影响到另一个。

但如果需要拷贝的对象只有一层,浅拷贝也很适用。

深拷贝

深拷贝可以将拷贝过程中遇到的引用类型都新开辟一块内存空间存放,这样可以避免子对象共享同一份内存的问题。

JSON方法

通过JSON.stringify()方法将对象转为JSON字符串,再通过JSON.parse()转回对象可实现深拷贝

let obj = {
  name:'张三',
  age:20,
  skill: {
    name: 'javascript'   
  }
}

let newObj = JSON.parse(JSON.stringify(obj))

obj.name = '李四'  
obj.skill = 'Node' 

// 实现深拷贝
console.log(obj)    // { name: '李四', age: 20, skill: { name: 'Node' } }
console.log(newObj) // { name: '张三', age: 20, skill: { name: 'javascript' } }

看上去不错,但是这个方法有个缺陷:无法拷贝特殊对象,如: RegExpDate,会出现各种问题,无法实现拷贝。

函数封装

封装一个深拷贝函数是一种比较不错的办法。通过递归实现多层次的拷贝,更具有适用性。

主要思路为 浅拷贝 + 递归

  • 基本数据类型直接拷贝
  • 引用数据类型进行递归拷贝
// 判断是否为引用数据类型
function isObject(target) {
  const type = typeof target
  return target !== null && (type === 'object' || type === 'function')
}

// 深拷贝
function deepClone(target) {
  // 基本数据类型直接返回
  if (!isObject(target)) return target 
  
  // 判断对象还是数组,返回一个对应类型的空变量
  let cloneTarget = Array.isArray(target) ? [] : {}
  Object.keys(target).forEach(key=> {
    // 循环赋值,通过递归拷贝下一层
    cloneTarget[key] = deepClone(target[key]) 
  })
  return cloneTarget
}

// 试一下
let obj = {
  name:'张三',
  age:20,
  skill: {
    name: 'javascript'   
  }
}

let newObj = deepClone(obj)

obj.name = '李四'  
obj.skill = 'Node' 

// 实现深拷贝
console.log(obj)    // { name: '李四', age: 20, skill: { name: 'Node' } }
console.log(newObj) // { name: '张三', age: 20, skill: { name: 'javascript' } }

总结

  1. 基本数据类型可以随意复制。
  2. 引用数据类型因为性能原因,变量保存的都是堆中对应的指针,直接复制的也是指针。
  3. 浅拷贝有多种方法,但是都只拷贝第一层,后续拷贝的都是指针
  4. 深拷贝用JSON方法转换可以对应大部分需求,一些特殊的对象除外。
  5. 封装深拷贝的思路为:遇到基本数据类型直接返回,复杂数据类型通过递归进行处理,实现层层拷贝。