JavaScript中实现浅拷贝与深拷贝最详解(多种版本实现,轻松解决面试题)

JavaScript中实现浅拷贝与深拷贝最详解(多种版本实现,轻松解决面试题)


浅拷贝很简单,因为它只涉及浅层的元素,所以一个循环直接 “=” 号 赋值就能实现。

深拷贝的实现就有点难度了,也是面试题常考的,因为它涉及的元素的深度不一,只要要拷贝的对象或数组内的子元素、子子元素、子子子元素… 再是对象或数组的话,就要再进行拷贝处理。所以深拷贝的实现方法也是很多的,下面将一 一讲解各种方法存在的问题和优化及其实现代码。

1. 浅拷贝

只进行浅层拷贝,相当于拷贝出来的对象或数组的属性都是被拷对象用 “=” 号赋值的。所以基本类型拷贝没问题, 属性值为对象或数组时拷贝的只是引用,指向同一内存地址

// 1. 使用ES6语法
function shallowClone1(target) {
  // 类型判断,target为对象或数组才有意义
  // typeof target === 'object' 有三种可能,对象、数组、null
  if (typeof target === 'object' && target !== null) {
    if (Array.isArray(target)) {
      // 如果target为数组
      return [...target]
    } else {
      // 为对象
      return { ...target }
    }
  } else {
  // 传入的参数不符合条件
    return target
  }
}

// 2. 使用基础语法
function shallowClone2(target) {
  // 类型判断,target为对象或数组才有意义
  // typeof target === 'object' 有三种可能,对象、数组、null
  if (typeof target === 'object' && target !== null) {
    // 根据target是对象或数组,创建对应的容器
    const result = Array.isArray(target) ? [] : {}
    // 遍历target
    for (let key in target) {
      // 检测该属性是否为对象本身的属性(不能拷贝原型对象的属性)
      if (target.hasOwnProperty(key)) {
      	// 往新容器赋值
        result[key] = target[key]
      }
    }
    // 返回结果
    return result
  } else {
    // 传入的参数不符合条件
    return target
  }
}

2. 深拷贝

进行深层拷贝,拷贝出来的对象内的 所有(包含深层)属性值为对象或数组的属性 都指向新的内存地址

实现深拷贝:

  • 实现1:大众乞丐版(js内置的JSON模块)
    • 问题1:函数属性会丢失
    • 问题2:循环引用会出错
  • 实现2:面试基础版(递归)
    • 解决问题1:函数属性还没丢失
  • 实现3:面试加强版(递归结合Map解决循环引用问题)
    • 解决问题2:循环引用正常
  • 实现4:最终版(优化遍历性能)
    • 数组:while | for | forEach() 优于 for-in | keys() & forEach()
    • 对象:for-in 与 keys() & forEach() 差不多
// ----------实现1:大众乞丐版(js内置的JSON模块)START-----------
function (target) {
  // 类型判断
  if (typeof target === 'object' && target !== null) {
  // 先通过 JS 格式的对象或数组创建 JSON 格式字符串
  // 再通过 JSON 字符串创建 JS 格式的对象或数组
    return JSON.parse(JSON.stringify(target))
  } else {
    return target
  }
}

// 问题1:函数属性丢失(JSON.stringify(),遇到属性为函数的会主动丢弃; JSON.parse()遇函数会报错)
deepClone1({ a: 1, b: '2', c: (a, b) => a + b }) // Object { a: 1, b: "2" }

 //问题2:循环引用会出错
 const obj1 = {a: 1, b: ['x', 'y'], c: {z: 100}}
 obj1.b.push(obj1.c)
 obj1.c.j = obj1.b
 deepClone(obj1) // 报错:Uncaught TypeError: Converting circular structure to JSON
 
 // 但单向引用不会出错,只是克隆之后是新对象,不再是引用关系
 const obj11 = {a: 1, b: ['x', 'y'], c: {z: 100}}
 obj11.b.push(obj11.c)
 const cobj11 = deepClone(obj11) // {a: 1, b: ['x', 'y', {z: 100}], c: {z: 100}}
 cobj11.c.z = 999
 console.log(cobj11) // {a: 1, b: ['x', 'y', {z: 100}], c: {z: 999}}
 
 // ----------实现1:大众乞丐版(js内置的JSON模块)END-----------


// ----------实现2:面试基础版(递归)START-----------
function deepClone2(target) {
  // 类型判断,如果不是对象或数组,则直接返回target
  if (typeof target === 'object' && target !== null) {
    // 根据类型创建一个容器
    const result = Array.isArray(target) ? [] : {}
    // 遍历对象
    for (let key in target) {
      // 检测该属性是否为对象本身的属性(不能拷贝原型对象的属性)
      if (target.hasOwnProperty(key)) {
        // 递归拷贝
        result[key] = deepClone2(target[key])
      }
    }
    // 遍历完毕,返回新的对象或数组
    return result
  } else {
    return target
  }
}

// 解决问题1:函数属性没丢失
deepClone2({ a: 1, b: '2', c: (a, b) => a + b }) // {a: 1, b: '2', c: ƒ}

// 循环引用问题没有解决。
// 原因:循环引用会使递归不断在循环引用的对象或数组中执行,造成死循环,最后栈溢出,浏览器报错
// 但单向引用不会出错,只是克隆之后是新对象,不再是引用关系

// ----------实现2:面试基础版(递归)END-----------


// ----------实现3:面试加强版(递归结合Map解决循环引用问题) START-----------

// 实现3实际就是在实现2的基础上结合了Map
// Map和对象类似,只不过Map的键和值可以是任意类型
// 用Map做容器可确保遍历时键的唯一性
function deepClone3(target, map = new Map()) {
  // 类型判断
  if (typeof target === 'object' && target !== null) {
    // 进行克隆前,判断对象或数组之前是否克隆过,如果克隆直接返回储存的值,阻断死循环
    if (map.get(target)) return map.get(target)
    let cache = map.get(target)
    // 根据类型创建一个容器
    const result = Array.isArray(target) ? [] : {}
    // 将新的结果存入到Map中,
    map.set(target, result)
    // 遍历对象
    for (let key in target) {
      // 检测该属性是否为对象本身的属性(不能拷贝原型对象的属性)
      if (target.hasOwnProperty(key)) {
        // 递归拷贝
        result[key] = deepClone3(target[key], map)
      }
    }
    return result
  } else {
    return target
  }
}
// 解决问题1:函数属性没丢失
//解决问题2:循环引用正常。循环引用其实就是不断的套娃,大家可以简单写段代码感受一下,如:
/* 
	const obj = { a: [1, 2, 3], b: { x: 'x', y: 'y' } }
	obj.a.push(obj.b)
	obj.b.z = obj.a
	console.log(obj) 
*/

// ----------实现3:面试加强版(递归结合Map解决循环引用问题) END-----------

// ----------实现4:最终版(优化遍历性能) START-----------
// 主要针对 实现3面试加强版 的优化
function deepClone4(target, map = new Map()) {
  // 类型判断
  if (typeof target === 'object' && target !== null) {
    // 进行克隆前,判断对象或数组之前是否克隆过,如果克隆直接返回储存的值
    if (map.get(target)) return map.get(target)
    let cache = map.get(target)
    // 根据类型创建一个容器
    let isArray = Array.isArray(target)
    const result = isArray ? [] : {}
    // 将新的结果存入到Map中,
    map.set(target, result)
    // 判断是对象还是数组,再进行遍历(优化点)
    if (isArray) {
      // 为数组,forEach() 遍历
      target.forEach((item, index) => {
        result[index] = deepClone4(item, map)
      })
    } else {
      // 为对象,keys() + forEach() 遍历
      Object.keys(target).forEach(key => {
        result[key] = deepClone4(target[key], map)
      })
    }
    return result
  } else {
    return target
  }
}
// ----------实现4:面试加强版2(优化遍历性能) END-----------
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值