Javascript的對象拷貝
在開始之前,我先普及一些基礎(chǔ)知識。Javascript 的對象只是指向內(nèi)存中某個位置的指針。這些指針是可變的,也就是說,它們可以重新被賦值。所以僅僅復(fù)制這個指針,其結(jié)果是有兩個指針指向內(nèi)存中的同一個地址。
- 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 中的對象時,要根據(jù)實際情況做一些考慮。
淺拷貝
如果要操作的對象擁有的屬性都是值類型,那么可以使用擴(kuò)展語法或 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" }
可以看到上面兩種方法都可以把多個不同來源對象中的屬性復(fù)制到一個目標(biāo)對象中。
- 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" }
上面這種方法是存在問題的,如果對象的屬性也是對象,那么實際被拷貝的只是那些指針,這跟執(zhí)行 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 } }
深拷貝(有限制)
想要對一個對象進(jìn)行深拷貝,一個可行的方法是先把對象序列化為字符串,然后再對它進(jìn)行反序列化。
- var obj = { a: 0, b: { c: 0 } };
- var copy = JSON.parse(JSON.stringify(obj));
不幸的是,這個方法只在對象中包含可序列化值,同時沒有循環(huán)引用的情況下適用。常見的不能被序列化的就是日期對象 —— 盡管它顯示的是字符串化的 ISO 日期格式,但是 JSON.parse 只會把它解析成為一個字符串,而不是日期類型。
深拷貝 (限制較少)
對于一些更復(fù)雜的場景,我們可以用 HTML5 提供的一個名為結(jié)構(gòu)化克隆的新算法。不過,截至本文發(fā)布為止,有些內(nèi)置類型仍然無法支持,但與 JSON.parse 相比較而言,它支持的類型要多的多:Date、RegExp、 Map、 Set、 Blob、 FileList、 ImageData、 sparse 和 typed Array。它還維護(hù)了克隆對象的引用,這使它可以支持循環(huán)引用結(jié)構(gòu)的拷貝,而這些在前面所說的序列化中是不支持的。
目前還沒有直接調(diào)用結(jié)構(gòu)化克隆的方法,但是有些新的瀏覽器特性的底層用了這個算法。所以深拷貝對象可能需要依賴一系列的環(huán)境才能實現(xiàn)。
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() };
- originaloriginal.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()都會給它們的第一個參數(shù)做一個結(jié)構(gòu)化克隆!需要注意的是,此方法是同步的,因為對瀏覽器歷史記錄進(jìn)行操作的速度不是很快,假如頻繁調(diào)用這個方法,將會導(dǎo)致瀏覽器卡死。
- const structuredClone = obj => {
- const oldState = history.state;
- history.replaceState(obj, null);
- const clonedObj = history.state;
- history.replaceState(oldState, null);
- return clonedObj;
- };
Via notification API:當(dāng)創(chuàng)建一個 notification 實例的時候,構(gòu)造器為它相關(guān)的數(shù)據(jù)做了結(jié)構(gòu)化克隆。需要注意的是,它會嘗試向用戶展示瀏覽器通知,但是除非它收到了用戶允許展示通知的請求,否則它什么都不會做。一旦用戶點擊同意的話,notification 會立刻被關(guān)閉。
- const structuredClone = obj => {
- const n = new Notification("", {data: obj, silent: true});
- nn.onshow = n.close.bind(n);
- return n.data;
- };
用 Node.js 進(jìn)行深拷貝
Node.js 的 8.0.0 版本提供了一個 序列化 api 可以和結(jié)構(gòu)化克隆相媲美. 不過這個 API 在本文發(fā)布的時候,還只是被標(biāo)記為試驗性的:
- const v8 = require('v8');
- const buf = v8.serialize({a: 'foo', b: new Date()});
- const cloned = v8.deserialize(buf);
- cloned.b.getMonth();
在 8.0.0 版本以下比較穩(wěn)定的方法,可以考慮用 lodash 的 cloneDeep函數(shù),它的思想多少也基于結(jié)構(gòu)化克隆算法。
結(jié)論
Javascript 中最好的對象拷貝的算法,很大程度上取決于其使用環(huán)境,以及你需要拷貝的對象類型。雖然 lodash 是最安全的泛型深拷貝函數(shù),但是如果你自己封裝的話,也許能夠獲得效率更高的實現(xiàn)方法,以下就是一個簡單的深拷貝,對 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);
- }
我很期待可以隨便使用結(jié)構(gòu)化克隆的那一天的到來,讓對象拷貝不再令人頭疼^_^