Javascript的对象拷贝[每日前端夜话0x53]
在开始之前,我先普及一些基础知识。Javascript 的对象只是指向内存中某个位置的指针。这些指针是可变的,也就是说,它们可以重新被赋值。所以仅仅复制这个指针,其结果是有两个指针指向内存中的同一个地址。
1var foo = { 2 a : "abc" 3} 4console.log(foo.a); 5// abc 6 7var bar = foo; 8console.log(bar.a); 9// abc1011foo.a = "yo foo";12console.log(foo.a);13// yo foo14console.log(bar.a);15// yo foo1617bar.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; 910 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 }1718 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}2627const structuredCloneAsync = window.structuredCloneAsync =28 StructuredCloner.prototype.cloneAsync.bind(new StructuredCloner);2930const main = async () => {31 const original = { date: new Date(), number: Math.random() };32 original.self = original;3334 const clone = await structuredCloneAsync(original);3536 // different objects:37 console.assert(original !== clone);38 console.assert(original.date !== clone.date);3940 // cyclical:41 console.assert(original.self === original);42 console.assert(clone.self === clone);4344 // equivalent values:45 console.assert(original.number === clone.number);46 console.assert(Number(original.date) === Number(clone.date));4748 console.log("Assertions complete.");49};5051main();
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 }1314 // Handle Array15 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 }2223 // Handle Function24 if (obj instanceof Function) {25 copy = function() {26 return obj.apply(this, arguments);27 }28 return copy;29 }3031 // Handle Object32 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 }3940 throw new Error("Unable to copy obj as type isn't supported " + obj.constructor.name);41}
我很期待可以随便使用结构化克隆的那一天的到来,让对象拷贝不再令人头疼^_^
©著作权归作者所有:来自51CTO博客作者mb5ff980b461ced的原创作品,如需转载,请注明出处,否则将追究法律责任更多相关文章
- Python数据科学:正则化方法
- 11种数据分析方法,别再说你不会了
- Python 为什么会有个奇怪的“...”对象?
- 学编程这么久,还傻傻分不清什么是方法(method),什么是函数(function)?
- 最全总结!聊聊 Python 操作PDF的几种方法(合并、拆分、水印、加密)
- 数据分析方法论
- 异步函数中的异常处理及测试方法 [每日前端夜话(0x18)]
- 面向对象综合练习(超市收银系统)
- Python 进阶之源码分析:如何将一个类方法变为多个方法?