Javascript的對象拷貝

翻譯:瘋狂的技術宅
原文: 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 進行深拷貝

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);
}

我很期待能夠隨便使用結構化克隆的那一天的到來,讓對象拷貝再也不使人頭疼^_^


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章


歡迎繼續閱讀本專欄其它高贊文章:

相關文章
相關標籤/搜索