一文带你了解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){ // 复制方法,
  //这个方法待完善,暂时未找到能够完美复制function的方案,如果有方案,望指出
  return obj
}
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);// true   由于方法单纯修改了引用的地址,所以这里是浅复制
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){ // 复制方法,
  //这个方法待完善,暂时未找到能够完美复制function的方案,如果有方案,望指出
  return obj
}
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 obj
//  由于typeof obj=='function'也符合下方的typeof obj!='object',所以此条可以省略
    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;
}