JS个人学习(5)——浅拷贝与深拷贝

359 阅读6分钟

浅拷贝

概念

JS的数据类型分为原始类型引用类型,当我们将一个引用类型的变量赋值给另一个变量时,往往会发生如下情况:

const obj1 = {name: 'wyt'};
const obj2 = obj1;
console.log(obj1.name);  // 'wyt'
obj2.name = 'huluntuntao';
console.log(obj2.name);  // 'huluntuntao'
console.log(obj1.name);  // 'huluntuntao'

由于引用类型的原因,会导致obj2改变之后obj1也随之改变,但是这种现象在我们很多情况下是不想让它存在的,因此就有了深拷贝与浅拷贝。

浅拷贝会创建一个新的对象,当拷贝对象的属性值为原始类型时,拷贝它的值,当拷贝对象的属性值为引用类型时,拷贝其引用地址。

简单点来说,就是浅拷贝可以拷贝对象属性的原始类型,但是拷贝对象属性的引用类型时,还是会产生上述情况。

也可以这么说,浅拷贝会创建一个新的地址存放想要拷贝的那个对象,但是对于对象内部其他引用类型的值,并不做处理。

接下去是几种常见的浅拷贝方法:

Object.assign()

Object.assign()算是在JS中比较常用到的浅拷贝方法:

const obj1 = {
    name: 'wyt',
    other: {
        age: 25
    }
};
const obj2 = Object.assign({}, obj1);
// 第一个参数是拷贝到目标对象,第二个参数是需要拷贝的对象;
console.log(obj1.name);  // 'wyt'
obj2.name = 'huluntuntao';
console.log(obj2.name);  // 'huluntuntao'
console.log(obj1.name);  // 'wyt'
obj2.other.age = 30;
console.log(obj1.other.age) // 30
console.log(obj2.other.age) // 30

以上代码也可以很明显的看出来,浅拷贝只能拷贝对象属性为原始类型的值,对于对象属性值为引用类型时,还是不能完全解决问题。

使用Object.assign()时的注意点:

  • 可以拷贝Symbol类型
  • 不能拷贝对象的继承属性
  • 不能拷贝不可枚举的属性
const obj1 = { a: { b: 1 }, sym: Symbol(1), [Symbol(2)]: 'hahah' };
Object.defineProperty(obj1, 'innumerable', {
    value: '这是一个不可枚举属性',
    enumerable: false
});
const obj2 = Object.assign({}, obj1);
console.log(obj2); // 结果如下图

1627311337(1).jpg

扩展运算符(...)

扩展运算符的使用如下,基本与Object.assign()没有差别:

const obj1 = {
    name: 'wyt',
    other: {
        age: 25
    }
};
const obj2 = {...obj1};
console.log(obj1.name);  // 'wyt'
obj2.name = 'huluntuntao';
console.log(obj2.name);  // 'huluntuntao'
console.log(obj1.name);  // 'wyt'
obj2.other.age = 30;
console.log(obj1.other.age) // 30
console.log(obj2.other.age) // 30

concat()和slice()

concat()和slice()只能对数组进行浅拷贝,都是会返回一个新的数组对象,并且不改变原数组。

自己实现一个浅拷贝

function MyAssign(source) {
    if (typeof source !== 'object' || source === null) {
        return source;
    }
    // 判断是数组还是对象,并根据判断创建初始值
    const target = Array.isArray(source) ? [] : {};
    // 循环遍历source
    for (let key in source) {
        // 判断当前属性是否为自身属性(hasOwnProperty()检测对象在排除原型链的情况下是否具有某个属性)
        if (source.hasOwnProperty(key)) {
            target[key] = source[key];
        }
    }
    return target;
}

什么是深拷贝

概念

浅拷贝在拷贝对象属性值为引用类型时,就只会拷贝属性值的引用地址。

深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。这两个对象是相互独立不受影响的,彻底实现了内存上的分离。

将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。

JSON.stringify()

JSON.stringify()是我们平时开发中比较常用的深拷贝方式,它通过先将对象序列化成string类型,再用JSON.parse()方式反序列化得到一个全新的对象,从而实现深拷贝。

const obj1 = {
    name: 'wyt',
    other: {
        age: 25
    }
};
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.other.age = 30;
console.log(obj2.other.age); // 30
console.log(obj1.other.age); // 25

JSON.stringify()虽然基本上能满足我们日常开发时深拷贝的需求,但是它其实存在许多问题:

  • 无法拷贝不可枚举属性
  • 对于正则对象(RegExp())拷贝后返回空对象({});
  • 对于日期对象(Date())拷贝后返回字符串;
  • 当属性值为undenfined、Symbol和函数时,其键值对会消失;
  • 无法拷贝对象原型链上的属性;
  • 拷贝循环引用的对象(即对象成环 (obj[key] = obj))时会报错;
  • 对象中含有NaN,Infinity和-Infinity的值时,序列化结果会返回null;
function Obj() {
    this.func = function () { alert(1) };
    this.obj = {a:1};
    this.arr = [1,2,3];
    this.und = undefined;
    this.reg = /123/;
    this.date = new Date(0);
    this.NaN = NaN;
    this.infinity = Infinity;
    this.sym = Symbol(1);
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
    enumerable:false,
    value:'innumerable'
});
console.log('obj1', obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);

1627319287(1).jpg

const obj1 = {
    name: 'wyt',
    other: {
        age: 30
    }
};
obj1.other.age = obj1;
const obj2 = JSON.stringify(obj1);

1627319386(1).jpg

简单版深拷贝实现

实现一下最基础的深拷贝:

function easyDeepClone(source) {
    if (typeof source !== 'object' || source === null) {
        return source;
    }
    // 判断是数组还是对象,并根据判断创建初始值
    const target = Array.isArray(source) ? [] : {};
    // 循环遍历source
    for (let key in source) {
        // 判断当前属性是否为引用类型
        if (typeof source[key] === 'object') {
            target[key] = easyDeepClone(source[key]);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

上述深拷贝函数还存在一些问题:

  • 无法拷贝不可枚举属性;
  • 这种方法只是针对普通的引用类型的值做递归复制,而对于 Date、RegExp、Function 这样的引用类型并不能正确地拷贝;
  • 无法解决循环引用的问题;

进阶版深拷贝实现

// 利用一个weakmap来开辟存储空间,存储当前对象和拷贝对象的对应关系。

function deepClone(source, weakmap = new WeakMap()) {
    // 判断是否为基本类型或者function,这里不对function做特别处理,可以直接返回
    const sourceType = typeof source;
    if ((sourceType !== 'object' && sourceType !== 'function') || source === null || sourceType === 'function') {
        return source;
    }
    // 判断是否为Date,如果是,则新建一个Date类型的并返回
    if (source.constructor === Date) {
        return new Date(source);
    }
    // 判断是否为RegExp,如果是,则新建一个RegExp类型的并返回
    if (source.constructor === RegExp) {
        return new RegExp(source);
    }
    if (weakmap.has(source)) {
        return weakmap.get(source);
    }
    let allDesc = Object.getOwnPropertyDescriptors(source);
    // 判断是数组还是对象,并根据判断创建初始值,如果是对象,遍历传入参数所有键的特性
    const target = Array.isArray(source) ? [] : Object.create(Object.getPrototypeOf(source), allDesc);
    weakmap.set(source, target);
    // 循环遍历source,利用Reflect.ownKeys()
    Reflect.ownKeys(source).map((key) => {
        if (typeof source[key] === 'object') {
            target[key] = deepClone(source[key], weakmap);
        } else {
            target[key] = source[key];
        }
    })
    return target;
}

Object.getOwnPropertyDescriptors()获取所指定对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。

Object.getPrototypeOf()获取对象的原型,如果没有继承属性,就返回null。

Reflect.ownKeys()获取对象所有属性,包括不可遍历的属性,但是不包括继承的属性。

WeakMap利用它弱引用的特点,可以不用手动清除内存,有效防止内存泄漏,优化代码。。。这里也是利用它解决了循环引用的问题。

以下是测试代码

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

深浅拷贝要点

  • 关于浅拷贝Object.assign()的用法与不足
  • 自己写一个浅拷贝
  • 深拷贝JSON.stringify()的用法与不足
  • 自己写一个最终的深拷贝,注意weakMap和Reflect.ownKeys()。

对于这个深拷贝,理解还是不够深刻,涉及到的东西比较多,上述的实现只是目前自己的理解范围内的,但是还有许多问题,如function的拷贝等。。。并且需要多多复习,才能牢记并巩固。