一文帶你了解js數據儲存及深復制(深拷貝)與淺復制(淺拷貝)


背景

在日常開發中,偶爾會遇到需要復制對象的情況,需要進行對象的復制。

由於現在流行標題黨,所以,一文帶你了解js數據儲存及深復制(深拷貝)與淺復制(淺拷貝)

理解

首先就需要理解 js 中的數據類型了
js 數據類型包含

  1. 基礎類型:StringNumbernullundefinedBoolean以及ES6引入的Symboles10中的BigInt
  2. 引用類型:Object

由於 js 對變量的儲存是棧內存堆內存完成的。

  • 基礎類型將數據保存在棧內存
  • 引用類型將數據保存在堆內存

由於 js 在數據讀取和寫入的時候,對基礎類型是直接讀寫棧內存中的數據,引用類型是將一個內存地址保存在棧內存中,讀寫都是修改棧內存中指向堆內存的地址

以如下代碼為例

let obj = {
  a:1,
  arr:[1,3,5,7,9],
  b:2,
  c:{
    num:100
  }
}
let num = 10

在內存中的表現為
內存中的展示

我們聲明個obj1

let obj1 = obj;
console.log(obj1 == obj);//true

因為這個賦值,把內存變成了這樣

賦值后

然后,內存中只是給js棧內存新增了一個指向堆內存的地址而已,這種就叫做淺復制。因為如圖可以看到,如果我們修改obj.a的話,實際修改的是堆內存0x88888888中的變量a,由於obj1也指向這個地址,所以obj1.a也被修改了

深復制是指,不單單復制引用地址,連堆內存都復制一遍,使objobj1不指向同一個地址。

代碼

分開來看深復制淺復制

淺復制

由上述圖可知,淺復制只是復制了堆內存的引用地址,通常在業務需求中出現的淺復制是指復制引用對象的第一層,也就是,基本類型復制新值,引用類型復制引用地址

淺復制可以使用的方案有循環賦值擴展運算符object.assign(),

let obj = {
  a:1,
  arr:[1,3,5,7,9],
  b:2,
  c:{
    num:100
  }
}
function clone1(obj){ // 使用循環賦值
  let b = {};
  for(let key in obj){
    b[key] = obj[key]
  }
  return b
}
function clone2(obj){ // 使用擴展運算符
  let b = {
    ...obj
  };
  return b
}
function clone3(obj){ // 使用object.assign()
  let b = {};
  Object.assign(b,obj)
  return b
}
let obj1 = clone1(obj);
let obj2 = clone2(obj);
let obj3 = clone3(obj);

console.log(obj1 === obj); //false 代表復制成功了
console.log(obj2 === obj); //false 代表復制成功了
console.log(obj3 === obj); //false 代表復制成功了
console.log('obj0.c.num修改前',obj.c.num); //100
console.log('obj1.c.num修改前',obj1.c.num); //100
console.log('obj2.c.num修改前',obj2.c.num); //100
console.log('obj3.c.num修改前',obj3.c.num); //100
obj0.c.num = 555;
console.log('obj0.c.num修改后',obj.c.num); //555
console.log('obj1.c.num修改后',obj1.c.num); //555
console.log('obj2.c.num修改后',obj2.c.num); //555
console.log('obj3.c.num修改后',obj3.c.num); //555

由於是淺復制,所以引用類型只是復制了內存地址,修改其中一個對象的子屬性后,引用這個地址的值都會被修改。

淺克隆圖解如下
淺克隆圖解

深復制

由於淺復制只是復制第一層,為了解決引用類型的復制,需要使用深復制來完成對象的復制,基本類型復制新值,引用類型開辟新的堆內存

深復制可以使用的方案有JSON.parse(JSON.stringify(obj))循環賦值

JSON.parse(JSON.stringify(obj))

let obj = {
  a:1,
  arr:[1,3,5,7,9],
  c:{
    num:100
  },
  fn:function(){
     console.log(1)
  },
  date:new Date(),
  reg:/\.*/g
}
function clone1(obj){ // 使用JSON.parse(JSON.stringify(obj))
  return JSON.parse(JSON.stringify(obj))
}
let obj1 = clone1(obj);
console.log(obj === obj1); //false 代表復制成功了
obj.c.num = 555;

console.log(obj.c.num,obj1.c.num) // 555,100

看起來是復制成功了!!~地址也變了,修改obj,obj1的引用地址不會跟着變化。

但是我們來console一下obj以及obj1

console.log(obj)
console.log(obj1)

打印結果

似乎發現了離奇的事情,只有obj.a以及obj.c正確的復制了,日期類型方法正則表達式均沒有復制成功,發生了一些奇怪的事情

循環賦值 deepClone

那么為了解決這種事情,就需要寫一個deepClone方法來完成深復制了,參考了許多開源庫的寫法,將所有的復制項單獨拆出,方便未來對特殊類型進行擴展,也防止不同功能間的變量互相干擾

 //既然是深復制,一定要傳入一個object,再return 一個新的 Object
function deepClone(obj){
    let newObj;
    if(obj instanceof Array){ // 數組的話,要new一個數組
      newObj = []
    }else if(obj instanceof Object){  // 對象的話,要new一個對象
      newObj = {}
    }
    if(obj === null) {
      return cloneNull(obj)
    }
    if(typeof obj=='function'){
        return cloneFunction(obj)
    }
    if(typeof obj!='object') {
        return cloneOther(obj)
    }
    if(obj instanceof RegExp) {
        return cloneRegExp(obj)
    }
    if(obj instanceof Date){
        return cloneDate(obj)
    }
    if(obj instanceof Array){
        for(let index in obj){
            newObj[index] = deepClone(obj[index]); // 對數組子項進行復制
        }
    }
    if(obj instanceof Object){
        for(let key in obj){
            newObj[key] = deepClone(obj[key]); // 對對象子項進行復制
        }
    }
    return newObj;
}
function cloneNull(obj){ // 復制NULL
  return obj
}
function cloneFunction(obj){ // 復制方法,
  // 復制一個新方法,將原方法轉成字符串,並new一個新的function
  return new Function('return '+obj.toString())()
}
function cloneOther(obj){ // 復制非對象的數據
  return obj
}
function cloneRegExp(obj){ // 復制正則對象
  return new RegExp(obj)
}
function cloneDate(obj){ // 復制日期對象
  return new Date(obj)
}

這樣一個基本上滿足功能的深復制就完成了。先測試一下

let obj = {
  a:1,
  arr:[1,3,5,7,9],
  c:{
    num:100
  },
  fn:function(){
     console.log(1)
  },
  date:new Date(),
  reg:/\.*/g
}

let obj1 = deepClone(obj);
console.log(obj.c === obj1.c); // false  代表復制成功
console.log(obj.fn === obj1.fn);// false  代表復制成功 
console.log(obj.date === obj1.date);// false  代表復制成功
console.log(obj.reg === obj1.reg);// false  代表復制成功

console一下

console.log(obj)
console.log(obj1)

打印結果

這樣,就完成了deepClone深復制方法

經過深復制后,圖解如下
深復制后內存圖解

優化 deepClone

上述代碼還有優化空間,參考了lodash庫,在進行 new 對象時,可以使用 constructor構造函數 來進行創建新的實例,這樣

  1. 可以不用判斷遞歸中,是數組還是對象
  2. 如果深復制的某一項是某個原型的實例,深復制完成后,依然是該原型的實例
function deepClone(obj){
    let newObj = new obj.constructor;
    if(obj === null) {
      return cloneNull(obj)
    }
    if(typeof obj=='function'){
        return cloneFunction(obj)
    }
    if(typeof obj!='object') {
        return cloneOther(obj)
    }
    if(obj instanceof RegExp) {
        return cloneRegExp(obj)
    }
    if(obj instanceof Date){
        return cloneDate(obj)
    }
    if(obj instanceof Array){
        for(let index in obj){
            newObj[index] = deepClone(obj[index]); // 對數組子項進行復制
        }
    }
    if(obj instanceof Object){
        for(let key in obj){
            newObj[key] = deepClone(obj[key]); // 對對象子項進行復制
        }
    }
    return newObj;
}
function cloneNull(obj){ // 復制NULL
  return obj
}
function cloneFunction(obj){ // 復制方法,
  // 復制一個新方法,將原方法轉成字符串,並new一個新的function
  return new Function('return '+obj.toString())()
}
function cloneOther(obj){ // 復制非對象的數據
  return obj
}
function cloneRegExp(obj){ // 復制正則對象
  return new RegExp(obj)
}
function cloneDate(obj){ // 復制日期對象
  return new Date(obj)
}

最終版本 deepClone

然后可以有一個合並版本的,比較節省代碼,將下方區分開的復制方法,合並到deepClone中,可以極大地減少代碼體積

function deepClone(obj){ //
    let newObj = new obj.constructor;
    if(obj === null) return obj
    if(typeof obj=='function') return new Function('return '+obj.toString())()
    if(typeof obj!='object') return obj
    if(obj instanceof RegExp) return new RegExp(obj)
    if(obj instanceof Date) return new Date(obj)
    // 運行到這里,基本上只存在數組和對象兩種類型了
    for(let index in obj){
        newObj[index] = deepClone(obj[index]); // 對子項進行遞歸復制
    }
    return newObj;
}


免責聲明!

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



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