【前端基础】2 - 8 深拷贝

§2-8 深拷贝

2-8.1 深浅拷贝

几乎每一门编程语言都会涉及到变量(基本数据类型和引用数据类型)的拷贝问题。一般地,我们希望拷贝得到的对象既拥有和源对象相同的属性和值,也希望修改二者其一的任意属性值,都不会影响对方。这种拷贝就被称为深拷贝(deep copy)。而修改任意一个对象的某个属性值,都能够在另一个对象上看到同步的更改,这种拷贝方式就成为浅拷贝(shallow copy)。

JavaScript 的一些标准 API 也提供了针对某些数据类型的拷贝方法(如 Object.assign(), Array.prototype.concat(), Array.prorotype.slice() 等)以及展开语法,但这些方法都是浅拷贝。

基本数据类型不存在深拷贝问题,它们一旦被复制,就相互独立,互不影响。真正可能出现浅拷贝问题的往往都是在引用数据类型上。

在 JavaScript 中实现深拷贝的方式有多种。在下文,我们都以下面所述的对象作为源对象,我们考虑对它的深拷贝实现。

// 示例源对象
// 参考自 CSDN @码飞_CC 的文章,链接见文章底部
const src = {
    // =========== 1.基础数据类型 ===========
    num: 0, // number
    str: '', // string
    bool: true, // boolean
    unf: undefined, // undefined
    nul: null, // null
    sym: Symbol('sym'), // symbol
    bign: BigInt(1n), // bigint

    // =========== 2.Object类型 ===========
    // 普通对象
    obj: {
        name: '我是一个对象',
        id: 1
    },
    // 数组
    arr: [0, 1, 2],
    // 函数
    func: function () {
        console.log('我是一个函数')
    },
    // 日期
    date: new Date(0),
    // 正则
    reg: new RegExp('/我是一个正则/ig'),
    // Map
    map: new Map().set('mapKey', 1),
    // Set
    set: new Set().add('set'),
    // =========== 3.其他 ===========
    [Symbol('1')]: 1  // Symbol作为key
};

// 4.添加不可枚举属性
Object.defineProperty(obj, 'innumerable', {
    enumerable: false,
    value: '不可枚举属性'
});

// 5.设置原型对象
Object.setPrototypeOf(obj, {
    proto: 'proto'
})

// 6.设置loop成循环引用的属性
obj.loop = obj

示例对象是一个包含了基本数据类型和引用数据类型的一个典型复合类型对象,需要格外注意该类型对象在深拷贝中可能出现的问题。

2-8.2 使用基础递归实现

我们定义一个函数 recurssiveClone(target, source) 用于递归实现深拷贝,将 source 对象中的数据拷贝到 target 中。考虑到基本数据类型不存在深拷贝问题,这里我们重点关注对象中可能出现的引用数据类型。而所有引用数据类型在原型链上都继承自 Object,因此,我们只需要在迭代对象属性的过程中,判断属性是否出现在 Object 的原型链上。若为真,则意味着我们需要深拷贝该对象,这一操作本身就属于函数的实现。因此只需要在条件为真时递归调用即可。

// 使用递归实现
function recurssiveClone(source) {
    if (typeof source === 'object' && source) {
        const target = {};
        // 迭代对象
        for (let k in source) {
            // 判断 key 是否为对象
            if (source[k] instanceof Object) {
                // 递归实现
                target[k] = {};
                recurssiveClone(target[k], source[k]);
            } 
            // 直接克隆基本数据类型
            else {
                target[k] = source[k];
            }
        }
    } else 
        return source;
}

// 声明目标对象并调用函数深拷贝
const tar = recurssiveClone(src);

但这种方法有一定缺陷:

  • 无法处理循环引用,会导致无限递归从而使得堆栈溢出。
  • 只考虑了 Object 对象,而 Array, Date, RegExp, Map, Set 对象都变成了 Object 对象,且值也不正确。
  • 丢失了属性名为 Symbol 类型的属性(因为其本身不可迭代)。
  • 丢失了不可枚举属性。
  • 原型上的属性也被添加到了目标对象中。

若要解决上述问题,我们需要修改上述递归实现,以满足我们的需求。改进后的算法详见文末链接。

2-8.3 使用 JSON 实现

我们也可以使用 JSON 的格式化字符串和解析方法深拷贝一个对象,这在本质上是读取源对象的内容后,以这些内容新建一个对象,二者独立。

// 使用 JSON 实现
function JSONClone(source) {
    if (typeof source === 'object' && source !== null)
        return JSON.parse(JSON.stringify(source));
    else
        return source;
}

但同样地,这个方法也存在缺陷:

  • 若源对象存在 BigInt 类型或循环引用,抛出错误。
  • Date 引用类型会变为字符串。
  • 对象中的值若为 Function, undefinedSymbol 类型,键值会消失。
  • 对象值为 Map, Set, RegExp 这几种类型,键值变成空对象。
  • 不可枚举属性、对象的原型链无法拷贝。
  • 其他情况(详见 JSON.stringify() 文档)。

2-8.4 使用 Lodash.js 实现

我们也可以使用现成的第三方 JavaScript 库实现深拷贝的功能。这里,我们采用 Lodash.js 实现。官方主页见文末链接。

<!-- 先引入 Lodash,js -->
<script src="
	https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js
"></script>
// 调用 Lodash.js 深拷贝
const tar = _.cloneDeep(src);

console.log(tar);
console.log(src);

2-8.3 参考链接

Shallow copy - MDN Web Docs Glossary: Definitions of Web-related terms | MDN (mozilla.org)

Deep copy - MDN Web Docs Glossary: Definitions of Web-related terms | MDN (mozilla.org)

JavaScript深拷贝看这篇就行了!(实现完美的ES6+版本)_javascript 深拷贝-CSDN博客

Lodash

posted @ 2024-03-26 22:52  Zebt  阅读(29)  评论(0)    收藏  举报