下次面試再問JavaScript怎麼實現深拷貝,我就不客氣了!

背景

你們都知道,JavaScript 中的基礎數據類型,好比 number, boolean, string, null, undefined 這些類型的變量在賦值的時候會分配獨立的內存空間。而複合類型,好比Object,這種類型的變量是引用型的,也就是保存內存的引用地址,可能多個變量指向的是同一個內存地址。這樣在修改變量的某個屬性時,其餘變量的屬性也跟着變了。面試

// 值類型
const a = 5;
let b = a; 
b = 6;
console.log(b) // 6
console.log(a) // 5

// 引用類型
const person1 = {
  name: 'tom',
};
let person2 = person1;
person2.name = 'jerry';

console.log(person1.name); // jerry
複製代碼

所以,這種狀況在有的時候會形成數據互相影響,致使意外的結果。這就須要用到對象的深拷貝了。深拷貝的方法有多種,面試的時候也常常會被問到。今天就來總結下,都有哪些經常使用的深拷貝實現方式。json

使用嵌套的展開操做符

const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = {name: original.name, work: {...original.work}};

// 拷貝成功
assert.deepEqual(original, copy);
// 確實是深拷貝
assert.ok(original.work !== copy.work);

複製代碼

經過 JSON 字符串

是一種取巧的方式,可是很是快捷。爲了深拷貝一個對象original,先把它轉成JSON字符串,而後再解析這個JSON字符串:數組

function jsonDeepCopy(original) {
  return JSON.parse(JSON.stringify(original));
}
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = jsonDeepCopy(original);
assert.deepEqual(original, copy);

複製代碼

這種方法的顯著缺點是,只能複製JSON格式支持的屬性名和值。bash

不支持的屬性名和值會直接忽略:微信

assert.deepEqual(
  jsonDeepCopy({
    [Symbol('a')]: 'abc',
    b: function () {},
    c: undefined,
  }),
  {} // empty object
);

複製代碼

其餘狀況會拋出異常:函數

assert.throws(
  () => jsonDeepCopy({a: 123n}),
  /^TypeError: Do not know how to serialize a BigInt$/);

複製代碼

實現通用深拷貝

下面是一個通用的深拷貝函數 :ui

function deepCopy(original) {
  if (Array.isArray(original)) {
    const copy = [];
    for (const [index, value] of original.entries()) {
      copy[index] = deepCopy(value);
    }
    return copy;
  } else if (typeof original === 'object' && original !== null) {
    const copy = {};
    for (const [key, value] of Object.entries(original)) {
      copy[key] = deepCopy(value);
    }
    return copy;
  } else {
    // 基礎類型無需拷貝
    return original;
  }
}

複製代碼

這個函數處理了三種狀況:this

  • 若是original是一個數組,咱們就建立一個新數組,並將original裏的元素深複製進去。
  • 若是original這一個對象,咱們使用相似的方法。
  • 若是original是原始類型的值,咱們什麼也不用作。

咱們嘗試調用一下 deepCopy():spa

const original = {a: 1, b: {c: 2, d: {e: 3}}};
const copy = deepCopy(original);

// 副本和原始值深度相等嗎?
assert.deepEqual(copy, original);

// 是否真的複製了全部層級
// (內容相等,對象不一樣)
assert.ok(copy     !== original);
assert.ok(copy.b   !== original.b);
assert.ok(copy.b.d !== original.b.d);

複製代碼

注意,deepCopy()只解決了展開操做符的一個問題。其餘問題仍然存在:原型沒有拷貝,特殊對象只有部分被拷貝,不可枚舉屬性被忽略,大部分屬性特被忽略。code

實現通用的完整拷貝幾乎是不可能的:並不是全部的數據都是樹狀的,有時候你不須要複製全部的屬性等等。

更簡潔版的deepCopy()

若是使用 .map() 和 Object.fromEntries(),前面實現的deepCopy() 能夠更加簡潔:

function deepCopy(original) {
  if (Array.isArray(original)) {
    return original.map(elem => deepCopy(elem));
  } else if (typeof original === 'object' && original !== null) {
    return Object.fromEntries(
      Object.entries(original)
        .map(([k, v]) => [k, deepCopy(v)]));
  } else {
    // 原始類型值無需拷貝
    return original;
  }
}

複製代碼

在類中實現深拷貝(進階)

實現類的實例拷貝,一般會用到兩種技術:

  • .clone() 方法
  • 複製構造器
.clone() 方法

這種技術爲須要實例深拷貝的類引入一個.clone()方法。它返回this的一個深拷貝。下面這個例子展現了三個能夠複製的類。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  clone() {
    return new Point(this.x, this.y);
  }
}
class Color {
  constructor(name) {
    this.name = name;
  }
  clone() {
    return new Color(this.name);
  }
}
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);
    this.color = color;
  }
  clone() {
    return new ColorPoint(
      this.x, this.y, this.color.clone()); // (A)
  }
}

複製代碼

帶註釋行 A 展現了這種技術的一個重要方面:複合型的實例屬性值必須也要遞歸地複製。

靜態的工廠方法

複製構造器是一種利用當前類的另外一個實例來初始化當前實例的構造器。複製構造器在靜態語言中很是流行,好比和C++和Java。你能夠經過靜態重載來提供多個版本的構造器。靜態的意思是它發生在編譯時。

在 JavaScript 中,你能夠這樣作(雖然不太優雅) :

class Point {
  constructor(...args) {
    if (args[0] instanceof Point) {
      // 複製構造器
      const [other] = args;
      this.x = other.x;
      this.y = other.y;
    } else {
      const [x, y] = args;
      this.x = x;
      this.y = y;
    }
  }
}

複製代碼

這個類的使用方式以下:

const original = new Point(-1, 4);
const copy = new Point(original);
assert.deepEqual(copy, original);

複製代碼

相反,JavaScript 中靜態工廠方法更合適。(靜態意味着它是類方法)

下面這個例子中,三個類 PointColor 和 ColorPoint 各有一個靜態工廠方法 .from()

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  static from(other) {
    return new Point(other.x, other.y);
  }
}
class Color {
  constructor(name) {
    this.name = name;
  }
  static from(other) {
    return new Color(other.name);
  }
}
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);
    this.color = color;
  }
  static from(other) {
    return new ColorPoint(
      other.x, other.y, Color.from(other.color)); // (A)
  }
}

複製代碼

帶註釋行A中,咱們再次用到了遞歸拷貝。

ColorPoint.from() 的用法以下:

const original = new ColorPoint(-1, 4, new Color('red'));
const copy = ColorPoint.from(original);
assert.deepEqual(copy, original);
複製代碼

下次面試再碰到這個問題,不用跟面試官客氣!

交流

歡迎關注微信公衆號「1024譯站」,獲取國際最新互聯網技術資訊。

公衆號:1024譯站
相關文章
相關標籤/搜索