翻译:疯狂的技术宅
原文: https://smalldata.tech/blog/2...
本文首发微信公众号:前端先锋
欢迎关注,天天都给你推送新鲜的前端技术文章javascript
在开始以前,我先普及一些基础知识。Javascript 的对象只是指向内存中某个位置的指针。这些指针是可变的,也就是说,它们能够从新被赋值。因此仅仅复制这个指针,其结果是有两个指针指向内存中的同一个地址。html
var foo = { a : "abc" } console.log(foo.a); // abc var bar = foo; console.log(bar.a); // abc foo.a = "yo foo"; console.log(foo.a); // yo foo console.log(bar.a); // yo foo bar.a = "whatup bar?"; console.log(foo.a); // whatup bar? console.log(bar.a); // whatup bar?
经过上面的例子能够看到,对象 foo 和 bar 都能随着对方的变化而变化。因此在拷贝 Javascript 中的对象时,要根据实际状况作一些考虑。前端
若是要操做的对象拥有的属性都是值类型,那么可使用扩展语法或 Object.assign(...)
java
var obj = { foo: "foo", bar: "bar" }; var copy = { ...obj }; // Object { foo: "foo", bar: "bar" } var obj = { foo: "foo", bar: "bar" }; var copy = Object.assign({}, obj); // Object { foo: "foo", bar: "bar" }
能够看到上面两种方法均可以把多个不一样来源对象中的属性复制到一个目标对象中。node
var obj1 = { foo: "foo" }; var obj2 = { bar: "bar" }; var copySpread = { ...obj1, ...obj2 }; // Object { foo: "foo", bar: "bar" } var copyAssign = Object.assign({}, obj1, obj2); // Object { foo: "foo", bar: "bar" }
上面这种方法是存在问题的,若是对象的属性也是对象,那么实际被拷贝的只是那些指针,这跟执行 var bar = foo;
的效果是同样的,和第一段代码中的作法同样。程序员
var foo = { a: 0 , b: { c: 0 } }; var copy = { ...foo }; copy.a = 1; copy.b.c = 2; console.dir(foo); // { a: 0, b: { c: 2 } } console.dir(copy); // { a: 1, b: { c: 2 } }
想要对一个对象进行深拷贝,一个可行的方法是先把对象序列化为字符串,而后再对它进行反序列化。面试
var obj = { a: 0, b: { c: 0 } }; var copy = JSON.parse(JSON.stringify(obj));
不幸的是,这个方法只在对象中包含可序列化值,同时没有循环引用的状况下适用。常见的不能被序列化的就是日期对象 —— 尽管它显示的是字符串化的 ISO 日期格式,可是 JSON.parse
只会把它解析成为一个字符串,而不是日期类型。算法
对于一些更复杂的场景,咱们能够用 HTML5 提供的一个名为结构化克隆的新算法。不过,截至本文发布为止,有些内置类型仍然没法支持,但与 JSON.parse
相比较而言,它支持的类型要多的多:Date、RegExp、 Map、 Set、 Blob、 FileList、 ImageData、 sparse 和 typed Array。 它还维护了克隆对象的引用,这使它能够支持循环引用结构的拷贝,而这些在前面所说的序列化中是不支持的。segmentfault
目前尚未直接调用结构化克隆的方法,可是有些新的浏览器特性的底层用了这个算法。因此深拷贝对象可能须要依赖一系列的环境才能实现。api
Via MessageChannels: 其原理是借用了通讯中用到的序列化算法。因为它是基于事件的,因此这里的克隆也是一个异步操做。
class StructuredCloner { constructor() { this.pendingClones_ = new Map(); this.nextKey_ = 0; const channel = new MessageChannel(); this.inPort_ = channel.port1; this.outPort_ = channel.port2; this.outPort_.onmessage = ({data: {key, value}}) => { const resolve = this.pendingClones_.get(key); resolve(value); this.pendingClones_.delete(key); }; this.outPort_.start(); } cloneAsync(value) { return new Promise(resolve => { const key = this.nextKey_++; this.pendingClones_.set(key, resolve); this.inPort_.postMessage({key, value}); }); } } const structuredCloneAsync = window.structuredCloneAsync = StructuredCloner.prototype.cloneAsync.bind(new StructuredCloner); const main = async () => { const original = { date: new Date(), number: Math.random() }; original.self = original; const clone = await structuredCloneAsync(original); // different objects: console.assert(original !== clone); console.assert(original.date !== clone.date); // cyclical: console.assert(original.self === original); console.assert(clone.self === clone); // equivalent values: console.assert(original.number === clone.number); console.assert(Number(original.date) === Number(clone.date)); console.log("Assertions complete."); }; main();
Via the history API:history.pushState()
和 history.replaceState()
都会给它们的第一个参数作一个结构化克隆!须要注意的是,此方法是同步的,由于对浏览器历史记录进行操做的速度不是很快,假如频繁调用这个方法,将会致使浏览器卡死。
const structuredClone = obj => { const oldState = history.state; history.replaceState(obj, null); const clonedObj = history.state; history.replaceState(oldState, null); return clonedObj; };
Via notification API: 当建立一个 notification 实例的时候,构造器为它相关的数据作告终构化克隆。须要注意的是,它会尝试向用户展现浏览器通知,可是除非它收到了用户容许展现通知的请求,不然它什么都不会作。一旦用户点击赞成的话,notification 会马上被关闭。
const structuredClone = obj => { const n = new Notification("", {data: obj, silent: true}); n.onshow = n.close.bind(n); return n.data; };
Node.js 的 8.0.0 版本提供了一个 序列化 api 能够和结构化克隆相媲美. 不过这个 API 在本文发布的时候,还只是被标记为试验性的:
const v8 = require('v8'); const buf = v8.serialize({a: 'foo', b: new Date()}); const cloned = v8.deserialize(buf); cloned.b.getMonth();
在 8.0.0 版本如下比较稳定的方法,能够考虑用 lodash 的 cloneDeep
函数,它的思想多少也基于结构化克隆算法。
Javascript 中最好的对象拷贝的算法,很大程度上取决于其使用环境,以及你须要拷贝的对象类型。虽然 lodash 是最安全的泛型深拷贝函数,可是若是你本身封装的话,也许可以得到效率更高的实现方法,如下就是一个简单的深拷贝,对 Date 日期对象也一样适用:
function deepClone(obj) { var copy; // Handle the 3 simple types, and null or undefined if (null == obj || "object" != typeof obj) return obj; // Handle Date if (obj instanceof Date) { copy = new Date(); copy.setTime(obj.getTime()); return copy; } // Handle Array if (obj instanceof Array) { copy = []; for (var i = 0, len = obj.length; i < len; i++) { copy[i] = deepClone(obj[i]); } return copy; } // Handle Function if (obj instanceof Function) { copy = function() { return obj.apply(this, arguments); } return copy; } // Handle Object if (obj instanceof Object) { copy = {}; for (var attr in obj) { if (obj.hasOwnProperty(attr)) copy[attr] = deepClone(obj[attr]); } return copy; } throw new Error("Unable to copy obj as type isn't supported " + obj.constructor.name); }
我很期待能够随便使用结构化克隆的那一天的到来,让对象拷贝再也不使人头疼^_^