专栏首页京程一灯Javascript的对象拷贝[每日前端夜话0x53]

Javascript的对象拷贝[每日前端夜话0x53]

正文共:1628 字

预计阅读时间:6分钟


翻译:疯狂的技术宅 原文:https://smalldata.tech/blog/2018/11/01/copying-objects-in-javascript

在开始之前,我先普及一些基础知识。Javascript 的对象只是指向内存中某个位置的指针。这些指针是可变的,也就是说,它们可以重新被赋值。所以仅仅复制这个指针,其结果是有两个指针指向内存中的同一个地址。

 1var foo = {
 2    a : "abc"
 3}
 4console.log(foo.a);
 5// abc
 6
 7var bar = foo;
 8console.log(bar.a);
 9// abc
10
11foo.a = "yo foo";
12console.log(foo.a);
13// yo foo
14console.log(bar.a);
15// yo foo
16
17bar.a = "whatup bar?";
18console.log(foo.a);
19// whatup bar?
20console.log(bar.a);
21// whatup bar?    

通过上面的例子可以看到,对象 foo 和 bar 都能随着对方的变化而变化。所以在拷贝 Javascript 中的对象时,要根据实际情况做一些考虑。

浅拷贝

如果要操作的对象拥有的属性都是值类型,那么可以使用扩展语法或 Object.assign(...)

1var obj = { foo: "foo", bar: "bar" };
2var copy = { ...obj };
3// Object { foo: "foo", bar: "bar" }
4var obj = { foo: "foo", bar: "bar" };
5var copy = Object.assign({}, obj);
6// Object { foo: "foo", bar: "bar" }

可以看到上面两种方法都可以把多个不同来源对象中的属性复制到一个目标对象中。

1var obj1 = { foo: "foo" };
2var obj2 = { bar: "bar" };
3var copySpread = { ...obj1, ...obj2 };
4// Object { foo: "foo", bar: "bar" }
5var copyAssign = Object.assign({}, obj1, obj2);
6// Object { foo: "foo", bar: "bar" }

上面这种方法是存在问题的,如果对象的属性也是对象,那么实际被拷贝的只是那些指针,这跟执行 var bar = foo; 的效果是一样的,和第一段代码中的做法一样。

1var foo = { a: 0 , b: { c: 0 } };
2var copy = { ...foo };
3copy.a = 1;
4copy.b.c = 2;
5console.dir(foo);
6// { a: 0, b: { c: 2 } }
7console.dir(copy);
8// { a: 1, b: { c: 2 } }

深拷贝(有限制)

想要对一个对象进行深拷贝,一个可行的方法是先把对象序列化为字符串,然后再对它进行反序列化。

1var obj = { a: 0, b: { c: 0 } };
2var copy = JSON.parse(JSON.stringify(obj));

不幸的是,这个方法只在对象中包含可序列化值,同时没有循环引用的情况下适用。常见的不能被序列化的就是日期对象 —— 尽管它显示的是字符串化的 ISO 日期格式,但是 JSON.parse 只会把它解析成为一个字符串,而不是日期类型。

深拷贝 (限制较少)

对于一些更复杂的场景,我们可以用 HTML5 提供的一个名为结构化克隆【https://developer.mozilla.org/en-US/docs/Web/API/Notification/Notification】的新算法。不过,截至本文发布为止,有些内置类型仍然无法支持,但与 JSON.parse 相比较而言,它支持的类型要多的多:Date、RegExp、 Map、 Set、 Blob、 FileList、 ImageData、 sparse 和 typed Array。它还维护了克隆对象的引用,这使它可以支持循环引用结构的拷贝,而这些在前面所说的序列化中是不支持的。

目前还没有直接调用结构化克隆的方法,但是有些新的浏览器特性的底层用了这个算法。所以深拷贝对象可能需要依赖一系列的环境才能实现。

Via MessageChannels: 其原理是借用了通信中用到的序列化算法。由于它是基于事件的,所以这里的克隆也是一个异步操作。

 1class StructuredCloner {
 2  constructor() {
 3    this.pendingClones_ = new Map();
 4    this.nextKey_ = 0;
 5
 6    const channel = new MessageChannel();
 7    this.inPort_ = channel.port1;
 8    this.outPort_ = channel.port2;
 9
10    this.outPort_.onmessage = ({data: {key, value}}) => {
11      const resolve = this.pendingClones_.get(key);
12      resolve(value);
13      this.pendingClones_.delete(key);
14    };
15    this.outPort_.start();
16  }
17
18  cloneAsync(value) {
19    return new Promise(resolve => {
20      const key = this.nextKey_++;
21      this.pendingClones_.set(key, resolve);
22      this.inPort_.postMessage({key, value});
23    });
24  }
25}
26
27const structuredCloneAsync = window.structuredCloneAsync =
28    StructuredCloner.prototype.cloneAsync.bind(new StructuredCloner);
29
30const main = async () => {
31  const original = { date: new Date(), number: Math.random() };
32  original.self = original;
33
34  const clone = await structuredCloneAsync(original);
35
36  // different objects:
37  console.assert(original !== clone);
38  console.assert(original.date !== clone.date);
39
40  // cyclical:
41  console.assert(original.self === original);
42  console.assert(clone.self === clone);
43
44  // equivalent values:
45  console.assert(original.number === clone.number);
46  console.assert(Number(original.date) === Number(clone.date));
47
48  console.log("Assertions complete.");
49};
50
51main();

Via the history API:history.pushState()history.replaceState() 都会给它们的第一个参数做一个结构化克隆!需要注意的是,此方法是同步的,因为对浏览器历史记录进行操作的速度不是很快,假如频繁调用这个方法,将会导致浏览器卡死。

1const structuredClone = obj => {
2  const oldState = history.state;
3  history.replaceState(obj, null);
4  const clonedObj = history.state;
5  history.replaceState(oldState, null);
6  return clonedObj;
7};

Via notification API:【https://developer.mozilla.org/en-US/docs/Web/API/Notification/Notification】当创建一个 notification 实例的时候,构造器为它相关的数据做了结构化克隆。需要注意的是,它会尝试向用户展示浏览器通知,但是除非它收到了用户允许展示通知的请求,否则它什么都不会做。一旦用户点击同意的话,notification 会立刻被关闭。

1const structuredClone = obj => {
2  const n = new Notification("", {data: obj, silent: true});
3  n.onshow = n.close.bind(n);
4  return n.data;
5};

用 Node.js 进行深拷贝

Node.js 的 8.0.0 版本提供了一个 序列化 api 【https://nodejs.org/api/v8.html#v8_serialization_api】可以和结构化克隆相媲美. 不过这个 API 在本文发布的时候,还只是被标记为试验性的:

1const v8 = require('v8');
2const buf = v8.serialize({a: 'foo', b: new Date()});
3const cloned = v8.deserialize(buf);
4cloned.b.getMonth();

在 8.0.0 版本以下比较稳定的方法,可以考虑用 lodash 的 cloneDeep函数,它的思想多少也基于结构化克隆算法。

结论

Javascript 中最好的对象拷贝的算法,很大程度上取决于其使用环境,以及你需要拷贝的对象类型。虽然 lodash 是最安全的泛型深拷贝函数,但是如果你自己封装的话,也许能够获得效率更高的实现方法,以下就是一个简单的深拷贝,对 Date 日期对象也同样适用:

 1function deepClone(obj) {
 2  var copy;
 3
 4  // Handle the 3 simple types, and null or undefined
 5  if (null == obj || "object" != typeof obj) return obj;
 6
 7  // Handle Date
 8  if (obj instanceof Date) {
 9    copy = new Date();
10    copy.setTime(obj.getTime());
11    return copy;
12  }
13
14  // Handle Array
15  if (obj instanceof Array) {
16    copy = [];
17    for (var i = 0, len = obj.length; i < len; i++) {
18        copy[i] = deepClone(obj[i]);
19    }
20    return copy;
21  }
22
23  // Handle Function
24  if (obj instanceof Function) {
25    copy = function() {
26      return obj.apply(this, arguments);
27    }
28    return copy;
29  }
30
31  // Handle Object
32  if (obj instanceof Object) {
33      copy = {};
34      for (var attr in obj) {
35          if (obj.hasOwnProperty(attr)) copy[attr] = deepClone(obj[attr]);
36      }
37      return copy;
38  }
39
40  throw new Error("Unable to copy obj as type isn't supported " + obj.constructor.name);
41}

我很期待可以随便使用结构化克隆的那一天的到来,让对象拷贝不再令人头疼^_^

求分享

如果你觉得这篇文章对你有帮助,请点击右下角的 “?好看” 并分享给小伙伴们↘️↘️↘️??

下面夹杂一些私货:也许你和高薪之间只差这一张图

2019年京程一灯课程体系上新,这是我们第一次将全部课程列表对外开放。

愿你有个好前程,愿你月薪30K。我们是认真的 !

在公众号内回复“体系”查看高清大图

本文分享自微信公众号 - 前端先锋(jingchengyideng)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-04-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • vue.js element表格数据刷新,页面重叠,router name重复

    在开始用vue.js element过程中,从网上抄的代码,遇到不少坑,记录一下。

    hotqin888
  • vue前端异常监控sentry实践

    但是onerror事件无法捕获到网络异常的错误(资源加载失败、图片显示异常等),例如img标签下图片url 404 网络请求异常的时候,onerror无法捕获到...

    csxiaoyao
  • JS 面试总结 理论篇

    由于浏览器可以渲染DOM,JS也可以修改DOM结构,未避免冲突,JS执行的时候,浏览器DOM渲染会停止。 两段JS不能同时执行。

    mafeifan
  • 8个用于设计漂亮表格的WordPress插件

    在WordPress中作为内容管理工具的一个好处是,几乎所有文字处理软件能做的事情(例如文本格式,布局格式,嵌入图像等等)都可以在WordPress编辑器中完成...

    丘壑
  • vue.js打包后放到beego的view目录下实现简单部署

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.n...

    hotqin888
  • 开源Dapper的Lambda扩展-Sikiro.Dapper.Extension V2.0

      去年我在业余时间,自己整了一套dapper的lambda表达式的封装,原本是作为了一个个人的娱乐项目,当时也只支持了Sql Server数据库。随之开源后,...

    陈珙
  • REdis主挂掉后复制节点才起来会如何?

    这种情况下复制节点(即从节点)无法提升为主节点,复制节点会一直尝试和主节点建立连接,直接成功。主节点恢复后,复制节点仍然保持为复制节点,并不会成为主节点。

    一见
  • ajax和axios、fetch的区别

    传统 Ajax 指的是 XMLHttpRequest(XHR), 最早出现的发送后端请求技术,隶属于原始js中,核心使用XMLHttpRequest对象,多个请...

    smy
  • beego结合vue.js实现简单部署

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.n...

    hotqin888
  • golang,beego+vue.js结合使用,超简单发布,超简单部署

    大家知道,golang开发的东西部署简单是它很大的卖点,但是当vue.js出现后,前端几乎都是它的天下了,因为用了vue.js就回不去了,无法再回到beego的...

    hotqin888

扫码关注云+社区

领取腾讯云代金券