资讯专栏INFORMATION COLUMN

摸索 JS 内深拷贝的最佳实践

Jonathan Shieber / 2930人阅读

摘要:想要简单点难道我深拷贝一个变量还要引入这么麻烦吗没有简单点的办法吗嗯,可能有点不是那么酷炫,但是他确实可以满足要求,而且也无须引入其他的库。

问题

由于 js 的传参方式有时会遇到这样的场景:

function setTime(data) {
  let result = {};
  result.obj = data.obj || {};
  result.obj.time = Date.now();
  return result
}

let data = {
  title:"loooook!",
  obj: {
    name: "keo",
    age: "12"
  }
}

let res = setTime(data);

console.log("res",res);
//res { obj: { name: "keo", age: "12", time: 1533625350183 } }
console.log("data",data);
//data { title: "loooook!", obj: { name: "keo", age: "12", time: 1533625350183 } }

我只是想继承参数的部分数据,并在此基础添加一些东西,但是参数 data 的源数据也被我改动了,如果之后有其他人想要从data获取数据,他可能还需要注意是否有像 setTime 这样的函数调用它。

一点修改
function setTime(data) {
  let result = {};
  result.obj =  {};
  Object.assign(result.obj,data.obj)
  result.obj.time = Date.now();
  return result
}

嗯,或者你也可以用 for...in,注意下二者的不同。
我们知道 Object.assign 只是浅拷贝,如果 data.obj 的属性值仍然有引用类型的话,那么还是会遇见同样的问题。
那要怎么办?难道要遍历data下每个属性的值?一个个复制过来?我们看看 lodash 是怎么做的

你猜的没错,的确是要深度遍历的。
baseClone方法内,拿到要拷贝的对象 value 后,先检查其类型,然后由对应的 handler 来处理,比如value是数组类型,则使 result 为同样长度的数据,然后对每一项都递归调用 baseClone,直到 value 是非引用类型,返回 value的值;如果是普通对象类型,则使 result 为空数组,然后拿取valuekey,对每个key的赋值也是递归调用baseClone

想要简单点

难道我深拷贝一个变量还要引入 lodash 这么麻烦吗 ?没有简单点的办法吗?

JSON.parse(JSON.stringify(param))

嗯,可能有点不是那么酷炫,但是他确实可以满足要求,而且也无须引入其他的库。但如果它真的这么完美,为什么 lodash 不这么写呢?
的确,它的缺点还挺多的,这里取几个我觉得比较重要的:

Set 类型、Map 类型以及 Buffer 类型会被转换成 {}

undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)

对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误

所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们

是啊,毕竟JSON的两个方法本身就只是用来转换 js 内的对象为 JSON 格式的,上述几点甚至都不是缺点,是我们想借用其他方法做深拷贝时遇到的问题。

既然是问题那应该可以解决吧,比如第一条和第二条,在 stringify 时判断类型,转化成 带类型标识符的对象字符串如:Set [1,2,3,4,5],然后在parse的时候对字符串进行解析,特别的类型调用对应的构造函数... 听起来变得更麻烦了,没关系,忍忍把各个类型的处理都写了;针对第三条,抛错了?没关系,我 try catch 包起来...,什么?循环引用?

循环引用?
function parse (param){
  return JSON.parse(JSON.stringify(param))
}

var a = {}
var b = {}
a["b"] = b
b["a"] = a

console.log(parse(a))
//TypeError: Converting circular structure to JSON at JSON.stringify

如上代码, 变量ab 互相引用对方,此时如果借用 JSON 的方法来进行深拷贝的话,会报循环结构转换转换 JSON 错误。这个问题怎么解决呢?我们再翻出 lodash 的源码看看...

      // Check for circular references and return its corresponding clone.
      stack || (stack = new Stack);
      var stacked = stack.get(value);
      if (stacked) {
        return stacked;
      }
      stack.set(value, result);

这里的 valueresult 分别是是一次遍历中 要拷贝的值 和 拷贝的结果。stack 是一个用来储存每次对应的 valueresult 的对象, stack下有一块用于储存的数组结构,该数组的每一项记录了单次遍历中的 valueresult,后二者再次以数组的形式存储,以 value 做为下标 0 的项,result 为下标 1 的项(这里不用对象的 key-value 形式可能是因为循环引用的变量无法使用 JSON.stringify 转换成字符串,只能 toString 转成 object Object);stack 是做为参数贯穿整个遍历过程的,每次遍历时都会以当前的 value 值进行查找(这里的查找直接是判断内存地址相等),如果能在 stack 中查到到对应的结果,则直接返回记录中的result,不再继续递归。
好了,循环引用的问题我们解决了,鼓掌!但是我也放弃使用 JSON 方法了...还有没有其他直接点的方法呢?

其他方法

结构化克隆算法是由HTML5规范定义的用于复制复杂JavaScript对象的算法,它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环。

怎么用?
emmm... 它还不能直接使用,你得依靠一些其他的 API ,间接的使用它。

postMessage()

function StructuredClone(param) {
  return new Promise(function (res, rej) {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => res(ev.data);
    port1.postMessage(param);
  })
}

StructuredClone(objects).then(result => console.log(result))

什么??还是异步的... 不,我希望能使用同步的方法使用它。

history()

function structuralClone(obj) {
  const oldState = history.state;
  history.replaceState(obj, document.title);
  const copy = history.state;
  history.replaceState(oldState, document.title);
  return copy;
}
const clone = structuralClone(objects);

如你所见,我们要借用一下 history.replaceState 这个方法,但是我们不能改变 history 原有的状态,所以用完就要恢复原状,当无事发生过。
至少,这是个同步的方法...,如果是同步的场景可以考虑一下...

性能展示

这里的测试代码是使用的 [Deep-copying in JavaScript] (https://dassur.ma/things/deep... 一文中的,并再次基础做了一些修改。

结果! (很懒就不画图表了)

单位 μs (缪斯),计算时间的用的接口是 performance.now()结果精确到5微秒。

chrome

safari

...em...Safari浏览器在调用完 postMessage 方法后就...没有然后了...表格都没刷出来...等了 40 s 终于刷出第一栏...
注释完 postMessage 又发现不能频繁的调用 history 。

firefox

...em.. 调用 history 相关 api 对 firefox 好像压力很大,以至于循环都有些错乱...于是注释了相关代码

就结果而言好像看不出什么区别,可能是我的数据不好,大家可以去看看原文,有展示阅读性更好的图表,尽管没有 lodash 就是了。

结果

回到我们最初的问题,我们只是想深拷贝一个 js 对象,如果只是一个比较"普通"的对象,用JSON的方法简单又快捷,但是如果这个对象有些“复杂”,似乎使用 lodash 的方法是比较好的选择,而且 lodash 连 Structured Clone 算法忽视的 symbol 类型 和 Function 也考虑其中,兼容性也没问题,也不会在不同的浏览器发生意外的状况...
lodash 万岁!lol!!

参考阅读:
Deep-copying in JavaScript

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96722.html

相关文章

  • 我用到ES6

    let和const webpack构建的项目,直接废弃var,直接使用let代替var for循环中使用let而不是var 变量声明之后就不会改变,请使用const 解构赋值 概念: 先解构再赋值,先从一堆数据中找出自己需要的数据,然后将找到的数据赋值给事先定义好的变量 // 对象的解构赋值 // 使用场景 // 1,等号右边是大json,等号左边是变量,这样可快速获取大json中数据,后续可...

    libin19890520 评论0 收藏0
  • 平时积累前端资源,持续更新中。。。

    本文收集学习过程中使用到的资源。 持续更新中…… 项目地址 https://github.com/abc-club/f... 目录 vue react react-native Weex typescript Taro nodejs 常用库 css js es6 移动端 微信公众号 小程序 webpack GraphQL 性能与监控 高质文章 趋势 动效 数据结构与算法 js core 代码规范...

    acrazing 评论0 收藏0
  • vuex重置所有state(可定制)

    摘要:这里为什么是一个数组呢因为这就是标题所描述的可定制,如果页面内重置绝大部分状态,但需要保留其中一些状态的时候我们可以通过我们传递过来的值来剔除相应的,使其不被更新。 在正式场景中我们经常遇到一个问题,就是登出页面或其他操作的时候,我们需要重置所有的vuex,让其变为初始状态,那么,就涉及到了多种方法:1、页面刷新: window.location.reload() 这个方法通过路由判断...

    singerye 评论0 收藏0
  • vuex重置所有state(可定制)

    摘要:这里为什么是一个数组呢因为这就是标题所描述的可定制,如果页面内重置绝大部分状态,但需要保留其中一些状态的时候我们可以通过我们传递过来的值来剔除相应的,使其不被更新。 在正式场景中我们经常遇到一个问题,就是登出页面或其他操作的时候,我们需要重置所有的vuex,让其变为初始状态,那么,就涉及到了多种方法:1、页面刷新: window.location.reload() 这个方法通过路由判断...

    LeviDing 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<