(译)最全的javaScript中对象深度拷贝指南

原文地址

我在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!

问题二:这种方式只支持基础类型,像MapSetRegExpDateArrayBuffer,函数对象等都会在序列化的时候弄丢

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),使用随机键名称生成给定深度和宽度的对象,并重新运行测试示例

图表统计

image

image

image

总结

  • 如果你不会使用循环对象并且不会使用内置类型,那么还是推荐使用JSON.parse。并且浏览器兼容性还更好(ie8+)
  • 如果在考虑性能和浏览器兼容,MessageChannel是最好的选择。(ie10+)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,550评论 0 5
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,639评论 18 139
  • 本博客转自:「作者:若愚链接:https://zhuanlan.zhihu.com/p/22361337来源:知乎...
    韩宝亿阅读 2,757评论 0 3
  • 《轻断食》:别一味相信、但确实有效,跟硬派健身配合着做,相当有效果! 《硬派健身》 《京都奈良一本就足够》 《畅游...
    cloverblue阅读 129评论 0 0
  • 我没有低头看着手机, 我只是无意间看到了窗外一个真实的世界。
    安小迈阅读 290评论 0 0