前言
今年初看到一篇好文章,今天才有时间翻译。
COPYING OBJECTS IN JAVASCRIPT
内容
在本文中,我们将着手于通过不同的方式探索在JavaScript中一个对象被拷贝。我们将会在浅拷贝和深拷贝之间做一个对比。
在开始之前,有一个小地方值得注意:在JS中的对象是一个简单的本地内存的引用。这些引用是可变的,比如,它们可以被重新赋值。因此,简单复制一份引用将导致2个引用对象指向同一个本地内存地址。
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都反应出改变会影响另一个对象。因此,在JS中做一个拷贝有时需要依赖于你的场景。
浅拷贝
如果你的对象只有值类型的属性,你可以使用解构赋值(spread)的语法或者Object.assign(...)
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" }
上面的两种方法可以被用于复制多种原对象或者目标对象的属性值:
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));
很不幸地,这个方法仅仅当对象包含可序列化地值类型和没有循环引用类型时起作用。用Date
对象就是一个不可序列化地值类型,尽管它在ISO的标准可以被打印成字符串,JSON.parse
仅仅把它解释成一个字符串(string),而不是Date
对象。
深拷贝的一些警告
更复杂的例子,你可以使用HTML5的一个新克隆算法,结构化克隆(structured clone)。很遗憾,在细这篇文章的时候,它仍然被限制于确定的类型,但是它比JSON.parse
支持更多的类型:Date, RegExp, Map, Set, Blob, FileList, ImageData, sparse and typed Array
。它也在被克隆的数据中保留了引用,支持上面提及的不起作用的序列化方法,循环和递归的结构。
目前,没有直接的方式可用于可结构化的克隆算法,但有一些新的浏览器特性可以使用这些算法。所以,有一些变通方法可以使用深拷贝对象。
Via MessageChannels
:它利用一个通信功能中的序列化算法。这个功能基于事件(event ),克隆结果是一个异步的操作。
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.replacState()
两个API,对它们的第一个参数(argument)创建了结构化克隆!注意在这里,这个方法是同步的,操作浏览器历史不是一个快的方式,一直调用这个方法会导致浏览器无法响应。
const structuredClone = obj => {
const oldState = history.state;
history.replaceState(obj, null);
const clonedObj = history.state;
history.replaceState(oldState, null);
return clonedObj;
};
Via this notification API
: 当创建一个新的通知(Notification),这个构造函数就会对关联的数据创建一个结构化克隆。通知将会展示一个浏览器的通知给用户,但是这个可能会没有征兆的失败,除非应用(浏览器)允许展示通知消息。在授予权限(通知打开)的情况下,通知会立即关闭。
const structuredClone = obj => {
const n = new Notification("", {data: obj, silent: true});
n.onshow = n.close.bind(n);
return n.data;
};
深拷贝在NodeJs中
在8.0.0的版本中,NodeJs提供了一个序列化的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
,它也是基于结构化克隆的算法。
总结
在JS中最好的复制对象的算法,高度依赖于上下文和对象的类型,当你想要拷贝的时候。lodash
是一个最安全的通用的深拷贝的方法,你可以有更有效的实现方式,如果你想要自定义,下面是对dates也起作用的深拷贝例子。
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);
}
就我而言,我期待在任何地方都能用结构化的克隆,而不用管浅拷贝深拷贝的问题,只有开开心心的复制。只想放养,每次放一只。
结束语
js的对象是引用类型,因此会引发很多问题。如果react中,PureComponent只有一层浅比较,如果传入的props是对象类型,它就会失效。比如vue中,只改变数组的下标,并不会触发双向数据绑定中的set监听。所以,理解深拷贝与浅拷贝是一项基本技能。
写作时间:20190811