博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
摸索 JS 内深拷贝的最佳实践
阅读量:7044 次
发布时间:2019-06-28

本文共 4132 字,大约阅读时间需要 13 分钟。

问题

由于 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 为空数组,然后拿取
value
key,对每个
key的赋值也是递归调用
baseClone

想要简单点

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

JSON.parse(JSON.stringify(param))复制代码

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

  1. Set 类型、Map 类型以及 Buffer 类型会被转换成 {}
  2. undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)
  3. 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误
  4. 所有以 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'] = bb['a'] = aconsole.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 方法了...还有没有其他直接点的方法呢?

其他方法

结构化克隆算法是的用于复制复杂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-copy/) 一文中的,并再次基础做了一些修改。

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

单位 μ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!!

参考阅读:

转载于:https://juejin.im/post/5b6aa4ede51d45198f5cc5b6

你可能感兴趣的文章
Activity的四种加载方式
查看>>
Babel的register特性使用
查看>>
用Flash如何制作360度产品展示
查看>>
lsb_release -a 查询系统版本
查看>>
Apache运维架构之Apache+PHP
查看>>
Python中filter、map、reduce、lambda 的用法
查看>>
我的友情链接
查看>>
Android PackageInstaller 静默安装的实现
查看>>
springboot+vue的前后端分离与合并方案
查看>>
Integer.toBinaryString() 的用法
查看>>
放开那个程序员!
查看>>
dataguard 创建物理standby
查看>>
Android帧缓冲区(Frame Buffer)硬件抽象层(HAL)模块Gralloc的实现原理分析(6)...
查看>>
内核挂钩调试记录
查看>>
我的友情链接
查看>>
Sed简介
查看>>
华为交换机接口收发报文的处理规则
查看>>
ios面试(转载)
查看>>
common digester-入门
查看>>
js - Array对象
查看>>