js学习-彻底搞清浅拷贝和深拷贝

243 阅读4分钟

什么是浅拷贝和深拷贝?

js中数据类型分为基本数据类型引用数据类型

基本类型值指的是直接保存在中的数据,即这种值是完全保存在内存中的一个位置。包含NumberStringBooleanNullUndefined ,Symbol

引用类型值指的在中保存指向中对象的地址,中保存此对象,所以引用类型的值保存的是一个指针。引用类型统称为 Object 类型。细分的话,有:Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型 等。

正因如此,当我们从一个变量向另一个变量复制引用类型的值时,实际上是将这个引用类型在内存中的引用地址复制了一份给新的变量,其实就是一个指针。因此当操作结束后,这两个变量实际上指向的是同一个在内存中的对象,所以改变其中任意一个对象,另一个对象也会跟着改变。

src=http___file.freexyz.cn_d_file_20200527_1cce7b563da9260a320653285ad71e83.png_20202412835&refer=http___file.freexyz.jpeg     因此深拷贝和浅拷贝只发生在引用类型中。简单来说他们的区别在于:


深浅拷贝的区别

1. 层次

  • 浅拷贝 只会将对象的各个属性进行依次复制,并不会进行递归复制,也就是说只会赋值目标对象的第一层属性。
  • 深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。

2. 是否开辟新的栈

  • 浅拷贝 对于目标对象第一层为基本数据类型的数据,就是直接赋值,即「传值」;而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址,即「传址」,并没有开辟新的栈,也就是复制的结果是两个对象指向同一个地址,修改其中一个对象的属性,则另一个对象的属性也会改变,
  • 深拷贝 而深复制则是开辟新的栈,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

方法实现

浅拷贝

利用遍历赋值的方式

function clone(obj) {
    var cloneObj = {}
    // for in 遍历,会遍历原型链里面的属性,所以需要排除原型链
    for(var key in obj) {
        if(obj.hasOwnProperty(key)) {
            cloneObj[key] = obj[key]
        }
    }
    return cloneObj
}
function clone(obj) {
    var cloneObj = {}
    // Object.keys()不会遍历到原型链中的属性
    for(var key of Object.keys(obj)) {
        cloneObj[key] = obj[key]
    }
    return cloneObj
}
function clone(obj) {
    var cloneObj = {}
    for(var [key, value] of Object.entries(obj)) {
        cloneObj[key] = value
    }
    return cloneObj
}

利用Array对象的方法(数组)

let arr = [1,2,3];
  • Array.concat()
const arr2 = obj.concat(); \\ 利用Array.concat()创建obj实现浅拷贝
  • Array.slice()
const arr2 = obj.slice(); \\ 利用Array.slice()截取全部obj实现浅拷贝
  • Array.from()
const arr2 = obj.from(); \\ 利用Array.from()复制obj实现浅拷贝

结构赋值

let arr2 = [...arr];
let obj2 = {...obj};

Object.assign()

function clone(obj) {
    return Object.assign(obj, {}) //利用Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
}

深拷贝

JSON.stringfy()JSON.parse()

根据不包含引用对象的普通数组深拷贝得到启发,不拷贝引用对象,拷贝一个字符串会新辟一个新的存储地址,这样就切断了引用对象的指针联系。

function deepClone(obj) {
    return JSON.parse(JSON.stringify(obj)) //利用JSON.stringify()方法将对象转化为JSON对象,再利用JSON.parse()将JSON对象转化为对象
}

缺点

(1)遇到函数,undefined,Symbol,Date对象时会自动忽略,遇到正则时会返回空对象

(2)无法拷贝obj对象原型链上的属性和方法

(3)当数据的层次很深,会栈溢出

所以JSON.parse(JSON.stringfy(X)),其中X只能是Number, String, Boolean, Array, 扁平对象,即那些能够被 JSON 直接表示的数据结构。

普通递归

function deepClone(obj) {
  //如果不是对象的话直接返回
  if (!isObject(obj)) return obj;
  //兼容数组
  let target = Array.isArray(obj) ? [] : {}
  //for in循环遍历
  for (var k in obj) {
    //判断是不是自己的属性
    if (obj.hasOwnProperty(k)) {
      if (typeof obj[k] === 'object') {
        //如果该属性还是个对象递归调用
        target[k] = deepClone(obj[k])
      } else {
        //反之,直接传值
        target[k] = obj[k]
      }
    }
  }
  //返回新对象
  return target
}
//判断是不是对象
function isObject(obj) {
  return typeof obj === 'object' && obj !== null
}

缺点

(1)无法保持引用

(2)当数据的层次很深,会栈溢出

防栈溢出

function cloneLoop(x) {
  const root = {};
  // 栈
  const loopList = [{
    parent: root,
    key: undefined,
    data: x,
  }];

  while (loopList.length) {
    // 深度优先
    const node = loopList.pop();
    const parent = node.parent;
    const key = node.key;
    const data = node.data;

    // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
    let res = parent;
    if (typeof key !== 'undefined') {
      // 判断data是否为数组
      res = parent[key] = Array.isArray(data) ? [] : {};
    }

    for (let k in data) {
      if (data.hasOwnProperty(k)) {
        if (typeof data[k] === 'object') {
          // 下一次循环
          loopList.push({
            parent: res,
            key: k,
            data: data[k],
          });
        } else {
          res[k] = data[k];
        }
      }
    }
  }

  return root;
}

优点

(1)不会栈溢出

(2)支持很多层级的数据

总结

其实深浅拷贝的方法还有很多,如果大家还知道什么办法,可以留言告诉我,我学习学习😁