如何实现一个深浅拷贝?(下)

简介: 浅拷贝是指,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值;如果拷贝的是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。

4. 手写实现浅拷贝


根据以上对浅拷贝的理解,实现浅拷贝的思路:

  • 对基础类型做最基本的拷贝;
  • 对引用类型开辟新的存储,并且拷贝一层对象属性。

代码实现:


// 浅拷贝的实现;
function shallowCopy(object) {
  // 只拷贝对象
  if (!object || typeof object !== "object") return;
  // 根据 object 的类型判断是新建一个数组还是对象
  let newObject = Array.isArray(object) ? [] : {};
  // 遍历 object,并且判断是 object 的属性才拷贝
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }
  return newObject;
}
复制代码


这里用到了 hasOwnProperty() 方法,该方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性。所有继承了 Object 的对象都会继承到 hasOwnProperty() 方法。这个方法可以用来检测一个对象是否是自身属性。


可以看到,所有的浅拷贝都只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝就无能为力了。深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝。


二、深拷贝的原理与实现


深拷贝是指,对于简单数据类型直接拷贝他的值,对于引用数据类型,在堆内存中开辟一块内存用于存放复制的对象,并把原有的对象类型数据拷贝过来,这两个对象相互独立,属于两个不同的内存地址,修改其中一个,另一个不会发生改变。


1. JSON.stringify()


JSON.parse(JSON.stringify(obj))是比较常用的深拷贝方法之一,它的原理就是利用JSON.stringifyJavaScript对象序列化成为JSON字符串),并将对象里面的内容转换成字符串,再使用JSON.parse来反序列化,将字符串生成一个新的JavaScript对象。


这个方法是目前我在公司项目开发中使用最多的深拷贝的方法,也是最简单的方法。

使用示例:

let obj1 = {  
  a: 0,
  b: {
    c: 0
  }
};
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.a = 1;
obj1.b.c = 1;
console.log(obj1); // {a: 1, b: {c: 1}}
console.log(obj2); // {a: 0, b: {c: 0}}
复制代码


这个方法虽然简单粗暴,但也存在一些问题,在使用该方法时需要注意:

  • 拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。
  • 无法拷贝不可枚举的属性;
  • 无法拷贝对象的原型链;
  • 拷贝 Date 引用类型会变成字符串;
  • 拷贝 RegExp 引用类型会变成空对象;
  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
  • 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。

在日常开发中,上述几种情况一般很少出现,所以这种方法基本可以满足日常的开发需求。如果需要拷贝的对象中存在上述情况,还是要考虑使用下面的几种方法。


2. 函数库lodash


该函数库也有提供_.cloneDeep用来做深拷贝,可以直接引入并使用:

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
复制代码


这里附上lodash中深拷贝的源代码供大家学习:

/**
* value:需要拷贝的对象
* bitmask:位掩码,其中 1 是深拷贝,2 拷贝原型链上的属性,4 是拷贝 Symbols 属性
* customizer:定制的 clone 函数
* key:传入 value 值的 key
* object:传入 value 值的父对象
* stack:Stack 栈,用来处理循环引用
*/
function baseClone(value, bitmask, customizer, key, object, stack) {
    let result
    // 标志位
    const isDeep = bitmask & CLONE_DEEP_FLAG    // 深拷贝,true
    const isFlat = bitmask & CLONE_FLAT_FLAG    // 拷贝原型链,false
    const isFull = bitmask & CLONE_SYMBOLS_FLAG // 拷贝 Symbol,true
    // 自定义 clone 函数
    if (customizer) {
        result = object ? customizer(value, key, object, stack) : customizer(value)
    }
    if (result !== undefined) {
        return result
    }
    // 非对象  
    if (!isObject(value)) {
        return value
    }
    const isArr = Array.isArray(value)
    const tag = getTag(value)
    if (isArr) {
        // 数组
        result = initCloneArray(value)
        if (!isDeep) {
            return copyArray(value, result)
        }
    } else {
        // 对象
        const isFunc = typeof value == 'function'
        if (isBuffer(value)) {
            return cloneBuffer(value, isDeep)
        }
        if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
            result = (isFlat || isFunc) ? {} : initCloneObject(value)
            if (!isDeep) {
                return isFlat
                    ? copySymbolsIn(value, copyObject(value, keysIn(value), result))
                  : copySymbols(value, Object.assign(result, value))
            }
        } else {
            if (isFunc || !cloneableTags[tag]) {
                return object ? value : {}
            }
            result = initCloneByTag(value, tag, isDeep)
        }
    }
    // 循环引用
    stack || (stack = new Stack)
    const stacked = stack.get(value)
    if (stacked) {
        return stacked
    }
    stack.set(value, result)
    // Map
    if (tag == mapTag) {
        value.forEach((subValue, key) => {
            result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
        })
        return result
    }
    // Set
    if (tag == setTag) {
        value.forEach((subValue) => {
            result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
        })
        return result
    }
    // TypedArray
    if (isTypedArray(value)) {
        return result
    }
    // Symbol & 原型链
    const keysFunc = isFull
      ? (isFlat ? getAllKeysIn : getAllKeys)
      : (isFlat ? keysIn : keys)
    const props = isArr ? undefined : keysFunc(value)
    // 遍历赋值
    arrayEach(props || value, (subValue, key) => {
        if (props) {
            key = subValue
            subValue = value[key]
        }
        assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    // 返回结果
    return result
}
复制代码


3. 手写实现深拷贝


(1)基础递归实现


实现深拷贝的思路就是,使用for in来遍历传入参数的属性值,如果值是基本类型就直接复制,如果是引用类型就进行递归调用该函数,实现代码如下:

function deepClone(source) {
      //判断source是不是对象
      if (source instanceof Object == false) return source;
      //根据source类型初始化结果变量
      let target = Array.isArray(source) ? [] : {}; 
      for (let i in source) {
        // 判断是否是自身属性
        if (source.hasOwnProperty(i)) {
          //判断数据i的类型
          if (typeof source[i] === 'object') {
            target[i] = deepClone(source[i]);
          } else {
            target[i] = source[i];
          }
        }
      }
      return target;
    }
console.log(clone({b: {c: {d: 1}}}));  // {b: {c: {d: 1}}})
复制代码


这样虽然实现了深拷贝,但也存在一些问题:

  • 不能复制不可枚举属性以及 Symbol 类型;
  • 只能对普通引用类型的值做递归复制,对于 Date、RegExp、Function 等引用类型不能正确拷贝;
  • 可能存在循环引用问题。


(2)优化递归实现

上面只是实现了一个基础版的深拷贝,对于上面存在的几个问题,可以尝试去解决一下:


  1. 使用 Reflect.ownKeys() 方法来解决不能复制不可枚举属性以及 Symbol 类型的问题。 Reflect.ownKeys() 方法会返回一个由目标对象自身的属性键组成的数组。它的返回值等同于: Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target));
  2. 当参数值为 Date、RegExp 类型时,直接生成一个新的实例并返回;
  3. 利用 Object.getOwnPropertyDescriptors() 方以获得对象的所有属性以及对应的特性。简单来说,这个方法返回给定对象的所有属性的信息,包括有关getter和setter的信息。它允许创建对象的副本并在复制所有属性(包括getter和setter)时克隆它。
  4. 使用 Object.create() 方法创建一个新对象,并继承传入原对象的原型链。Object.create()方法会创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
  5. 使用 WeakMap 类型作为 Hash 表,WeakMap 是弱引用类型,可以防止内存泄漏,所以可以用来检测循环引用,如果存在循环,则引用直接返回 WeakMap 存储的值。WeakMap的特性就是,保存在其中的对象不会影响垃圾回收,如果WeakMap保存的节点,在其他地方都没有被引用了,那么即使它还在WeakMap中也会被垃圾回收回收掉了。在深拷贝的过程当中,里面所有的引用对象都是被引用的,为了解决循环引用的问题,在深拷贝的过程中,希望有个数据结构能够记录每个引用对象有没有被使用过,但是深拷贝结束之后这个数据能自动被垃圾回收,避免内存泄漏。


代码实现:

function deepClone (obj, hash = new WeakMap()) {
  // 日期对象直接返回一个新的日期对象
  if (obj instanceof Date){
    return new Date(obj);
  } 
  //正则对象直接返回一个新的正则对象     
  if (obj instanceof RegExp){
    return new RegExp(obj);     
  }
  //如果循环引用,就用 weakMap 来解决
  if (hash.has(obj)){
    return hash.get(obj);
  }
  // 获取对象所有自身属性的描述
  let allDesc = Object.getOwnPropertyDescriptors(obj);
  // 遍历传入参数所有键的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
  hash.set(obj, cloneObj)
  for (let key of Reflect.ownKeys(obj)) { 
    if(typeof obj[key] === 'object' && obj[key] !== null){
      cloneObj[key] = deepClone(obj[key], hash);
    } else {
      cloneObj[key] = obj[key];
    }
  }
  return cloneObj
}
复制代码


可以使用以下数据进行测试:

let obj = {
  num: 1,
  str: 'str',
  boolean: true,
  und: undefined,
  nul: null,
  obj: { name: '对象', id: 1 },
  arr: [0, 1, 2],
  func: function () { console.log('函数') },
  date: new Date(1),
  reg: new RegExp('/正则/ig'),
  [Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
  enumerable: false, value: '不可枚举属性' 
});
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj    // 将loop设置成循环引用的属性
let cloneObj = deepClone(obj)
console.log('obj', obj)
console.log('cloneObj', cloneObj)
复制代码


运行结果如下:

网络异常,图片无法展示
|
可以看到,这样基本就实现了多数数据类型的深拷贝,不过也还存在一些缺陷,比如Map和Set结构在这个方法中无法进行拷贝,大家可以自己实现一下。

相关文章
|
7月前
|
JSON JavaScript 前端开发
怎么做深拷贝?
怎么做深拷贝?
|
7月前
|
XML JSON 前端开发
前端深浅拷贝各有哪些方法,优缺点
前端深浅拷贝各有哪些方法,优缺点
73 0
【面试题精讲】深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
【面试题精讲】深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
|
7月前
|
存储 人工智能 前端开发
深拷贝浅拷贝的区别?如何实现一个深拷贝?
深拷贝浅拷贝的区别?如何实现一个深拷贝?
108 0
|
6月前
|
存储 编译器 C++
C++进阶之路:何为拷贝构造函数,深入理解浅拷贝与深拷贝(类与对象_中篇)
C++进阶之路:何为拷贝构造函数,深入理解浅拷贝与深拷贝(类与对象_中篇)
53 0
|
3月前
|
存储 JSON 前端开发
栈在前端中的应用,顺便再了解下深拷贝和浅拷贝!
该文章探讨了栈在前端开发中的应用,并深入讲解了JavaScript中深拷贝与浅拷贝的区别及其实现方法。
栈在前端中的应用,顺便再了解下深拷贝和浅拷贝!
|
7月前
|
C++
C++什么是深浅拷贝,深浅拷贝的区别?
C++什么是深浅拷贝,深浅拷贝的区别?
|
人工智能 JavaScript 前端开发
为什么要用深浅拷贝、什么是深浅拷贝、以及如何实现
首先我们要明白一点,js中数据类型分为: 基本数据类型 (Number, String, Boolean, Null, Undefined, Symbol) 对象数据类型 ( Object )** 引用数据类型的值是保存在栈内存和堆内存中的对象。栈区内存保存变量标识符和指向堆内存中该对象的指针。当寻找引用值时,解释器会先寻找栈中的地址。然后根据地址找到堆内存的实体
113 0
为什么要用深浅拷贝、什么是深浅拷贝、以及如何实现
|
存储 JavaScript 前端开发
深拷贝浅拷贝有什么区别?怎么实现深拷贝?
深拷贝浅拷贝有什么区别?怎么实现深拷贝?
97 0