理解JS深拷貝


前言:

  JS的拷貝(copy),之所以分為深淺兩種形式,是因為JS變量的類型存在premitive(字面量)與reference(引用)兩種區別。當然,大多數編程語言都存在這種特性。

  眾所周知,內存包含的結構中,有堆與棧。在JS里,字面量類型變量存放在棧中,儲存的是它的值,而引用類型變量雖然在棧中也占有空間,但儲存的只是一個內存地址(通過該地址可以索引找到真實結構所在的內存區域),它的真實結構是存在於堆中的。如下圖所示:

  結合圖示來看,一般來說,淺拷貝只是拷貝了內存棧中的數據,深拷貝,則是要沿着引用類型變量的真實內存地址,去進行一次次的深度遍歷,直到拷貝完目標遍歷在棧與堆中的所有真實值。

一、淺拷貝的實現

  JS實現了一些擁有淺拷貝功能的接口,比如解構賦值的rest模式、Object.assgin。

  但淺拷貝的缺陷在於,進行拷貝之后,如果改變了被拷貝目標的某個引用屬性的值,則拷貝結果的對應屬性的值也會發生改變,反過來亦是如此。

  比如將example['愛好'][0]賦予新值 ('聽歌'),如下圖所示。

  從本質上來說,就是因為兩者都指向同一個內存區域。那片內存區域一旦發生了變動,自然兩者取到的值都發生改變,而且完全一樣。

二、深拷貝的實現

  深拷貝的原理,前文已經敘述過,但過於抽象,還不夠具體。

  JS里,可以利用原生的JSON序列化與反序列化接口組合進行實現深拷貝。

  如下圖所示,深拷貝的結果與被拷貝的目標之間,已經互不影響。

  

  不過,JSON方式實現的深拷貝,有很多缺陷,首先,是拷貝失真:

  1. 值為undefined、函數、Symbol的屬性,或者鍵為Symbol字符串的屬性,拷貝后,屬性會丟失。

  2. 值為NaN的屬性,拷貝后,值轉為了null。

  3. 值為非標准對象Object,比如Set、Map、Error、RegExp等等的屬性或數組元素,拷貝后,值轉為了空的標准對象,丟失了原來的原型繼承關系。

  3. 值為undefined、NaN、函數的數組元素,拷貝后,值轉為了null。

  其次,是拷貝功能的缺陷:

  1. 原型鏈丟失

  2. 無法拷貝有循環引用的對象

  綜上所述,要實現比較完整功能的深拷貝,就必須得兼顧JSON方式的功能和缺點。

三、手動實現深拷貝

  追尋深拷貝的實現方式,可以理解為:深拷貝 = 淺拷貝+深度遍歷+特殊情況容錯。

  以下,我們來實現一個深拷貝函數,deepCopy。假定函數接受的輸入為o。

// 深拷貝函數
function deepCopy(o) {

}

深拷貝的基本實現思路,從JS數據類型的角度出發,可以先區分字面量與引用兩種類型的變量。

然后,只要判斷是字面量,我們就直接淺拷貝返回,否則就進入深度遍歷,重復前面的淺拷貝,直到遍歷結束。

// 深拷貝函數
function deepCopy(o) {
  // 如果是字面量,直接返回
  if(isPrimitive(o)) return o;
  // 否則,進行深度遍歷

  /** 
   * 深度遍歷代碼 
   */
}

我們先實現一個判斷輸入是否為字面量的函數

function isPrimitive(o) {
  if (typeof o !== 'function' && typeof o !== 'object') return true;
  if (o === null) return true;
  return false;
}

然后,進行深度遍歷。深度遍歷一般有兩種選擇,一個是遞歸,一個是while循環。

遞歸很好理解,但有個缺陷,大量函數棧幀的入棧,很容易導致內存空間不足而爆棧,特別是對於有循環引用關系的輸入,可能秒秒鍾爆炸。這里,我們采用while循環。

采用while循環的話,我們可以模擬一個棧結構,棧如果為空,則結束循環,若不為空,則進行循環,循環第一步,先出棧,然后處理數據,處理完之后,進入下一次循環判斷。

在JS里,模擬棧結構可以用數組,push與pop組合,完美實現后入先出。在數據結構與算法里,這叫深度優先。

// 深拷貝函數
function deepCopy(o) {
  // 如果是字面量,直接返回; 否則,進行深度遍歷
  if(isPrimitive(o)) return o;

  // 首先,先定義一個觀察者,用來記錄遍歷的結果。等到遍歷結束,這個觀察者就是深拷貝的結果。
  const observer = {};

  // 然后,用數組模擬一個棧結構
  const nodeList = [];

  // 其次,為了每次遍歷時能地做一些處理,入棧的數據用對象來表示比較合適。
  nodeList.push({
    key: null, // 這里,增加一個key屬性,用來關聯每次遍歷所要處理的數據。
  });

  // 循環遍歷
  while(nodeList.length > 0) {
    const node = nodeList.pop(); // 出棧,深度優先

    // 處理節點node

  }
}

接下來,就是處理節點node了。這里要處理的任務,主要有:

1.特殊情況處理,比如Symbol類型的屬性雖然無法被Object.keys迭代出來,但可以用Reflect.ownKeys來解決。又比如,針對循環引用,可以在循環外面建立哈希表,每次循環都判斷要處理的輸入是否已存在哈希表,如果存在,直接引用,否則,存入哈希表。

// 用WeakMap模擬的哈希表,它的弱引用特性可以避免內存泄露
const hashmap = new WeakMap();

// 遍歷包括Symbol類型在內的所有屬性
const keys = Reflect.ownKeys(node.value);

2.初始化,將輸入o掛載到節點里,並存入哈希表。

// 初始化
if (node.key === null) {
  node.value = o;
  node.observer = observer
  // 存入哈希表
  hashmap.set(node.value, node.observer)
}

3.對節點的屬性進行遍歷,屬性值為引用類型,將它壓入棧,否則,觀察者利用關聯的key記錄屬性值,然后進入下一次循環。

for (let i = 0; i < keys.length; i++) {
  key = keys[i];
  value = node.value[key];
  // 是字面量,直接記錄
  if (isPrimitive(value)) {
    node.observer[key] = value;
    continue;
  }
  // 否則,入棧
  nodeList.push({
    key,
    value,
    observer: node.observer
  })
}

4.每次對節點屬性進行遍歷前,先根據哈希表進行判斷

// 查詢哈希表,如果不存在對象key,就存入哈希表
if (!hashmap.has(node.value)) {
  hashmap.set(node.value, node.observer[node.key] = isArray(node.value) ? [] : {});
  // 將對象壓入棧
  nodeList.push({
    key: node.key,
    value: node.value,
    observer: node.observer[node.key]
  })
  continue;
}
// 存在哈希表里,則從哈希表里取出,賦值
else if (node.observer !== hashmap.get(node.value)) {
  node.observer[node.key] = hashmap.get(node.value)
  continue;
}

這里,補上isArray函數,用來判斷是否為數組

function isArray(o) {
  return Object.prototype.toString.call(o) === '[object Array]';
}

到此,深拷貝函數已經成型了。但,還不夠完善,因為還沒有對輸入是函數的情況做處理。

所以,添加兩個函數,一個判斷是否是函數,一個用例拷貝函數。

// 判斷函數
function isFunction(o) {
  return Object.prototype.toString.call(o) === '[object Function]';
}

// 拷貝函數
function copyFunction(fnc) {
  const f = eval(`(${fnc.toString()})`)
  Object.setPrototypeOf(f, Object.getPrototypeOf(fnc))
  Object.keys(fnc).map(key => f[key] = deepCopy(fnc[key]))
  return f;
}

循環遍歷之前,加一層對函數的判斷

// 是函數,則拷貝函數
  if (isFunction(o)) return copyFunction(o);

遍歷的時候,也要加一層對函數的判斷

// 函數直接賦值
else if (isFunction(node.value)) { node.observer[node.key] = copyFunction(node.value) continue; }

循環結束后,我們還要對原型鏈進行處理,深拷貝,不能把繼承關系給弄丟,這也是輸入無論是數組還是對象都能獲得正確拷貝結果的一個技巧

// 繼承原型
Object.setPrototypeOf(observer, Object.getPrototypeOf(o))

最后,返回觀察者對象,即深拷貝結果。

  // 返回深拷貝結果
  return observer;

四、測試結果與結論

 

五、手動實現的深拷貝完整代碼

 

 

// 判斷字面量
function isPrimitive( o) {
   if ( typeof o !== 'function' && typeof o !== 'object') return true;
   if (o === null) return true;
   return false;
}
// 判斷數組
function isArray( o) {
   return Object.prototype.toString. call(o) === '[object Array]';
}
// 判斷函數
function isFunction( o) {
   return Object.prototype.toString. call(o) === '[object Function]';
}
// 拷貝函數
function copyFunction( fnc) {
   const f = eval( `( ${fnc. toString() } )`)
   Object. setPrototypeOf(f, Object. getPrototypeOf(fnc))
   Object. keys(fnc). map( key => f[key] = deepCopy(fnc[key]))
   return f;
}
// 哈希表
const hashmap = new WeakMap();
// 深拷貝函數
function deepCopy( o) {
   // 如果是字面量,直接返回; 否則,進行深度遍歷
   if ( isPrimitive(o)) return o;
   // 是函數,則拷貝函數
   if ( isFunction(o)) return copyFunction(o);
   // 首先,先定義一個觀察者,用來記錄遍歷的結果。等到遍歷結束,這個觀察者就是深拷貝的結果。
   const observer = {};
   // 然后,用數組模擬一個棧結構
   const nodeList = [];
   // 其次,為了每次遍歷時能地做一些處理,入棧的數據用對象來表示比較合適。
  nodeList. push({
    key: null, // 這里,增加一個key屬性,用來關聯每次遍歷所要處理的數據。
  });
   // 提升變量,盡量少在循環里創建變量
   let node;
   let keys;
   let key;
   let value;
   // 循環遍歷
   while (nodeList.length > 0) {
    node = nodeList. pop(); // 出棧,深度優先
     // 處理節點node
     // 初始化
     if (node.key === null) {
      node.value = o;
      node.observer = observer
       // 存入哈希表
      hashmap. set(node.value, node.observer)
    }
     // 是函數,則直接記錄
     else if ( isFunction(node.value)) {
      node.observer[node.key] = copyFunction(node.value)
       continue;
    }
     // 查詢哈希表,如果不存在對象key,就存入哈希表
     else if ( !hashmap. has(node.value)) {
      hashmap. set(node.value, node.observer[node.key] = isArray(node.value) ? [] : {});
       // 是對象,入棧
      nodeList. push({
        key: node.key,
        value: node.value,
        observer: node.observer[node.key]
      })
       continue;
    }
     // 存在哈希表里,則從哈希表里取出,賦值
     else if (node.observer !== hashmap. get(node.value)) {
      node.observer[node.key] = hashmap. get(node.value)
       continue;
    }
     // 遍歷包括Symbol類型的所有屬性
    keys = Reflect. ownKeys(node.value);
     for ( let i = 0; i < keys.length; i ++) {
      key = keys[i];
      value = node.value[key];
       // 是字面量,直接存儲
       if ( isPrimitive(value)) {
        node.observer[key] = value;
         continue;
      }
       // 否則,入棧
      nodeList. push({
        key,
        value,
        observer: node.observer
      })
    }
  }
   // 繼承原型
   Object. setPrototypeOf(observer, Object. getPrototypeOf(o))
   // 返回深拷貝結果
   return observer;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM