你不知道的深复制黑科技

本文探讨了JavaScript中深复制的实现,从简单的浅复制问题出发,深入到深复制的各种方法,包括使用JSON、递归及避免循环引用的策略。同时,文章提到了for-in、Object.keys和Object.getOwnPropertyNames等属性获取方法,并附带了深度优先和广度优先的测试,以及利用JSON的黑科技。最后,作者分享了关于面试和职位内推的信息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

由简入繁

今天花点时间,来整理一波深复制的skr操作。由简入繁 实现一个不会循环引用的deepClone方法

深复制&&潜复制

文章开头,先扫个盲,仔细看下面这个例子

  var obj = {a: 1, b: 2, c: { a: 3 },d: [4, 5]} 
  var obj1 = obj 
  var obj2 = JSON.parse(JSON.stringify(obj))//深拷贝常用方法 
  var obj3 = {...obj} 
  var obj4 = Object.assign({},obj) 
  obj.a = 999 
  obj.c.a = -999 
  obj.d[0] = 123 
  console.log(obj1) //{a: 999, b: 2, c: { a: -999 },d: [123, 5]} 
  console.log(obj2) //{a: 1, b: 2, c: { a: 3 },d: [4, 5]} 
  console.log(obj3) //{a: 1, b: 2, c: { a: -999 },d: [123, 5]} 
  console.log(obj4) //{a: 1, b: 2, c: { a: -999 },d: [123, 5]}

发现什么问题了吗?这些操作好像只是遍历了树的第一层,然后将指针指向了新的对象

  • 这里有个知识点:有人可能会觉得基本类型 是在栈中开辟一个新空间存放,其实obj.a 和obj1.a 开始用的是同一个‘1’,后面进行obj.a=111操作的时候 开辟一个新的空间存放999,obj1.a还是存放‘1’

单纯可爱001

    以下代码可能会忽略类型校验、各种兼容,只要看思路就好0.0

  // 处理对象 var obj = { a: {b: {c: 123}} }
  function clone1 (obj) {
  	var result = {}
    for(var i in obj) {
        if (obj.hasOwnProperty(i)) {
            if (typeof obj[i] === 'object') {
				result[i] = clone(obj[i])
            } else {
                result[i] = obj[i]
            }
        }
    }
    return result
  }
  • 这里的类型判断有点问题,完整版本看这里
  • 想知道为什么用hasOwnProperty,请戳这里
  • 其实还可以思考下,如果兼容数组 要怎么做,如果是es6的 还会有set、map、weakset、weakmap

上面这这种方法 长相丑陋,而且在深度优先或者循环引用的时候 容易栈溢出(貌似广度优先不会),所以我们要想个办法
暂时思路是:破坏引用或者做循环引用检查,我们接下来会重点介绍前者。

简单粗暴002

项目中,很多人会通过JSON提供的两个方法来实现深复制

  JSON.parse(JSON.stringify(obj))
  var a = {}
  a.a = a
  JSON.parse(JSON.stringify(a)) // Uncaught TypeError: Converting circular structure to JSON

但是通过测试发现,复制深度是1000000的对象,直接栈溢出了,说明底层也是使用的递归的方式。而且还一个有趣的现象,JSON内部做了循环引用检查

  • JSON不可以复制对象的方法,莫慌 猛戳这里找黑科技

高冷帅气003

想要破解递归爆栈,方法只有一个:不用递归。我们可以尝试下上学时候老师教的链表还有堆栈
我们把一个对象旋转90°,像极了一棵树,那么我们面对的问题就成了 复制一个树
链表太复杂了,还有记录指针,我们可以通过栈+js对象这个技巧,规避掉还原树的时候 找不到节点顺序关系的难题,假装看不到注释的代码

function deepClone (obj) {
  // const store = []
  let root = Array.isArray(obj) ? [] : {}

  const loopList = [{parent: root, key: undefined, data: obj}]

  while (loopList.length) {
    // 深度优先 广度优先没影响吧
    const node = loopList.pop()
    const parent = node.parent
    const key = node.key
    const data = node.data

    let resource = parent
    if (typeof key !== 'undefined') {
	  // 如果只是对象 这里可以使用连续赋值
      resource = parent[key] = Array.isArray(data) ? [] : {}
    }

    // let exitedData = find(store, data)
    // if (exitedData) {
    //   parent[key] = exitedData.target
    //   continue
    // }

    // store.push({ data, target: resource })

    for (let k in data) {
      if (data.hasOwnProperty(k)) {
        if (typeof data[k] === 'object') {
          loopList.push({parent: resource, key: k, data: data[k]})
        } else {
          resource[k] = data[k]
        }
      }
    }
  }
  return root
}

var O = {
  aaa: {bbb: {ccc: 456},},
  ddd: [123, 456, 789]
}

var A = [
  {aaa: {bbb: {ccc: 456},}},
  132
]

var ooo = deepClone(O)
var aaa = deepClone(A)

目前情况是:递归没有了,但还是没有破除循环引用。最蛋疼的是还出现了一个新的问题,关系丢失,具体如下

var a = {}
var b = { c: a, d: a}
var e = clone(b)
e.c === e.d // false

然后我们想到,可不可以找一个store做缓存,一来可以打破循环引用,二来 还可以空间换时间 骚秀一波
打开注释,见证奇迹的时刻到了 skr skr skr
骚完之后,我们回归现实 看看这个方法的缺点:

  • 如果你不想保持对象的关联关系,不要用此方法
  • 如果对象数量很多,不要用此方法

附录

Types方法

var type = (param) => {
  var checker = Object.prototype.toString
  var getName = () => {
    return checker.call(param).slice(8).slice(0, -1).toLowerCase()
  }
  var is = (typeName) => {
    return checker.call(param) === `[object ${typeName}]`
  }
  var isOneOf = (names = []) => {
    return names.some(name => checker.call(param) === `[object ${name}]`)
  }
  var isEvery = (names = []) => {
    return names.every(name => checker.call(param) === `[object ${name}]`)
  }
  var isNot = (typeNames = []) => {
    var checkers = {
      string: () => !is(typeNames),
      array: () => typeNames.every(name => !is(name))
    }
    return checkers[getName(typeNames)]()
  }
  // 暂时闭包够用了,后面用涉及到this了 记得bind
  return { is, isOneOf, isEvery, isNot, name: getName() }
}

for-in、Object.keys、Object.getOwnPropertyNames的用法

  • for-in:输出原型链+自身所有可枚举的属性
  • keys:输出自身可枚举属性,就是for-in + hasOwnProperty
  • getOwnPropertyNames:输出自身全部属性

测试深度优先+广度优先

function dataAction(deep, breadth) {
    var data = {}, pointer = data
    for (var i = 0; i < deep; i++) {
        pointer = pointer['data'] = {}
        for (var j = 0; j < breadth; j++) {
            pointer[j] = j
        }
    }
    return data
}
// test
clone1(dataAction(n,m))

JSON黑科技

// 这个 parse 比较牛逼 可以不丢失function
// 注意:不要搞太大的对象 storage最大5M
const JSONBooster = {
  stringify: (toJson) => {
    return JSON.stringify(toJson, function (key, val) {
      if (typeof val === 'function') { return `0?0:${val}` }
      return val
    })
  },
  parse: (str) => {
    return JSON.parse(str, function (k, v) {
      if (v.indexOf && v.indexOf('function') > -1) {
        return evil(`(function(){return ${v} })()`)
      }
      return v
    })
  }
}

// eval的代替方案 貌似可行
const evil = (fn) => {
  var Fn = Function
  return new Fn('return ' + fn)()
}

跪求简历

字节跳动飞书业务内推咯~
在线办公,未来可期!
北京、杭州、武汉、广州、深圳、上海,六大城市等你来投~
感兴趣的朋友可以私我咨询&内推,也可以通过链接直接投递!
海量hc,极速响应~
传送门

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值