我在 JavaScript 中如何拷贝一个对象? 这是一个简单的问题, 但是答案确不是很简单
Did you ever wanted to create a deep copy of an object in JavaScript? There is a way, but you are not gonna like it... I feel like we need something better pic.twitter.com/IDazhB8BKJ Surma (@dassurma) 2018 年 1 月 22 日
引用调用
JavaScript 通过引用来传递所有的值如果你不知道这是什么意思, 下面有个例子:
- function mutate(obj) {
- obj.a = true;
- }
- const obj = {a: false};
- mutate(obj)
- console.log(obj.a); // prints true
mutate 方法改变了作为参数传递进来的这个对象在值调用环境中, 这个函数式传递的这个值, 所以相关于这个函数是执行了一个拷贝这个函数使这个对象对外是不可见的但是在像 js 的这种引用调用的环境, 将会得到这个真实的对象所以最后控制台输出的为 true
不过, 你想要保持你的原始的对象, 其他函数只是创建了这个对象的拷贝
在下面就介绍几种深度拷贝的方式
JSON.parse
第一种最古老的方式就是通过将对象转换为 JSON 字符串格式, 然后将其转换为对象
- let obj = { name : "huyue" };
- let copy = JSON.parse(JSON.stringify(obj));
- obj.name = 'hy';
- console.log(copy);//'huyue'
但是这种方式有些问题
问题一: 当对象中出现循环引用的时候会报错尽管你可能认为你不会如此使用, 但是那些还是会很容易发生比如当你构建了树状类型的数据机构的时候, 其中一个节点引用了父级的某个节点, 这样就出现了这种场景
- const x = {};
- const y = {x};
- x.y = y; // Cycle: x.y.x.y.x.y.x.y.x...
- const copy = JSON.parse(JSON.stringify(x)); // throws!
问题二: 这种方式只支持基础类型, 像 Map,Set,RegExp,Date,ArrayBuffer, 函数对象等都会在序列化的时候弄丢
- var source = { name:function(){console.log(1);}, child:{ name:"child" } }
- var target = JSON.parse(JSON.stringify(source));
- console.log(target.name); //undefined
注: JSON 对象是 ES5 中引入的新的类型 (支持的浏览器为 IE8+), 浏览器支持情况
结构化克隆
结构化克隆是一个现有算法, 它是被用来把一个领域的值传递到另一个比如, 你调用 postMessage 去发送一个消息给另一个窗口或 webWorker 结构化很好的地方就是他能处理循环对象, 并且支持多种内置类型
MessageChannel
我们通过 MessageChannel 创建一个新的消息通道, 并通过它的两个 MessagePort 属性来发送数据和获取数据我们接受到的这条信息就是会包含原始数据的结构化克隆对象但是这种方式是异步情况, 所以下面例子使用的 async awit 实现了的, 也可参见在线地址
- function structuralClone(obj) {
- return new Promise(resolve => {
- const {port1, port2} = new MessageChannel();
- port2.onmessage = ev => resolve(ev.data);
- port1.postMessage(obj);
- });
- }
- const obj = /* ... */;
- const clone = await structuralClone(obj);
注: 浏览器支持 IE10+, 浏览器支持力度情况
History API
如果你曾经使用过
history.pushState()
去构建一个 SPA(单页应用), 你应该会知道能提供一个状态对象去保存这 URL 这个状态对象就是结构化克隆, 并且还是同步的我们一定要小心, 避免在使用这个状态对象的时候去混淆任何程序逻辑, 所以我们需要在我们克隆了之后去恢复这个原始的状态对象为了防止发生任何事件, 请使用 history.replaceState() 而不是 history.pushState()replaceState 和 pushState 区别详情
- function structuralClone(obj) {
- const oldState = history.state;
- history.replaceState(obj, document.title);
- const copy = history.state;
- history.replaceState(oldState, document.title);// 就是为了恢复原始状态对象, 避免干扰
- return copy;
- }
- const obj = /* ... */;
- const clone = structuralClone(obj);
为了复制一个对象, 使用浏览器的引擎感觉有些笨拙不过你还是可以这么做, 有些事情还是得注意, 因为 Safari 浏览器会限制 30 秒内调用 relaceState 的次数上限为 100 次
注: 浏览器支持 IE10+, 浏览器支持力度情况
Notification API
这种方式由 Jeremy Banks 建议, 通知接口用于向用户配置和显示桌面通知, 这个消息通知的 api 有一个与它们相关的数据对象被克隆看到这, 可能有的人表示有点不是很明白, 那么可以点击在线示例
- function structuralClone(obj) {
- return new Notification('', {data: obj, silent: true}).data;
- }
- const obj = /* ... */;
- const clone = structuralClone(obj);
它基本触犯了浏览器内的权限机制, 所以怀疑这个可能会非常慢出于某种原因, Safari 浏览器总是返回 undefined 可以使用在线示例
注: 浏览器不支持 IE, 浏览器支持力度情况
性能测试
对上面几种方式进行性能测试看哪种方式性能最高刚开始尝试时, 我拿一个小 JSON 对象, 并通过这些克隆对象一千次的不同方式来进行测试幸运的是, Mathias Bynens 告诉我在给一个对象增加属性的时候 V8 是有缓存为了确保不走缓存, 所以我写了一个 [函数](a function that generates objects of given depth and width using random key names), 使用随机键名称生成给定深度和宽度的对象, 并重新运行测试示例
图表统计
总结
如果你不会使用循环对象并且不会使用内置类型, 那么还是推荐使用 JSON.parse 并且浏览器兼容性还更好 (ie8+)
如果在考虑性能和浏览器兼容, MessageChannel 是最好的选择 (ie10+)
来源: https://juejin.im/post/5a82f4a16fb9a0633d71dc14