目录
深拷贝的最终实现
1. JavaScript数据类型的拷贝原理
2. 深浅拷贝
2.1 浅拷贝
2.2 深拷贝
方法1:JSON.stringify()
方法2:递归基础版深拷贝
方法3:递归一文带你详细了解JavaScript中的深拷贝
首页 web前端 js教程 一文带你详细了解JavaScript中的深拷贝

一文带你详细了解JavaScript中的深拷贝

Oct 21, 2022 pm 07:37 PM
javascript

一文带你详细了解JavaScript中的深拷贝

网上有很多关于深拷贝的文章,但是质量良莠不齐,有很多都考虑得不周到,写的方法比较简陋,难以令人满意。本文旨在完成一个完美的深拷贝,大家看了如果有问题,欢迎一起补充完善。

评价一个深拷贝是否完善,请检查以下问题是否都实现了:

  • 基本类型数据是否能拷贝?

  • 键和值都是基本类型的普通对象是否能拷贝?

  • Symbol作为对象的key是否能拷贝?

  • DateRegExp对象类型是否能拷贝?

  • MapSet对象类型是否能拷贝?

  • Function对象类型是否能拷贝?(函数我们一般不用深拷贝)

  • 对象的原型是否能拷贝?

  • 不可枚举属性是否能拷贝?

  • 循环引用是否能拷贝?

怎样?你写的深拷贝够完善吗?

深拷贝的最终实现

这里先直接给出最终的代码版本,方便想快速了解的人查看,当然,你想一步步了解可以继续查看文章余下的内容:

function deepClone(target) {
    const map = new WeakMap()
    
    function isObject(target) {
        return (typeof target === '一文带你详细了解JavaScript中的深拷贝ect' && target ) || typeof target === 'function'
    }

    function clone(data) {
        if (!isObject(data)) {
            return data
        }
        if ([Date, RegExp].includes(data.constructor)) {
            return new data.constructor(data)
        }
        if (typeof data === 'function') {
            return new Function('return ' + data.toString())()
        }
        const exist = map.get(data)
        if (exist) {
            return exist
        }
        if (data instanceof Map) {
            const result = new Map()
            map.set(data, result)
            data.forEach((val, key) => {
                if (isObject(val)) {
                    result.set(key, clone(val))
                } else {
                    result.set(key, val)
                }
            })
            return result
        }
        if (data instanceof Set) {
            const result = new Set()
            map.set(data, result)
            data.forEach(val => {
                if (isObject(val)) {
                    result.add(clone(val))
                } else {
                    result.add(val)
                }
            })
            return result
        }
        const keys = Reflect.ownKeys(data)
        const allDesc = Object.getOwnPropertyDescriptors(data)
        const result = Object.create(Object.getPrototypeOf(data), allDesc)
        map.set(data, result)
        keys.forEach(key => {
            const val = data[key]
            if (isObject(val)) {
                result[key] = clone(val)
            } else {
                result[key] = val
            }
        })
        return result
    }

    return clone(target)
}
登录后复制

1. JavaScript数据类型的拷贝原理

先看看一文带你详细了解JavaScript中的深拷贝图(除了Object,其他都是基础类型):
一文带你详细了解JavaScript中的深拷贝
在JavaScript中,基础类型值的复制是直接拷贝一份新的一模一样的数据,这两份数据相互独立,互不影响。而引用类型值(Object类型)的复制是传递对象的引用(也就是对象所在的内存地址,即指向对象的指针),相当于多个变量指向同一个对象,那么只要其中的一个变量对这个对象进行修改,其他的变量所指向的对象也会跟着修改(因为它们指向的是同一个对象)。如下图:
一文带你详细了解JavaScript中的深拷贝

2. 深浅拷贝

深浅拷贝主要针对的是Object类型,基础类型的值本身即是复制一模一样的一份,不区分深浅拷贝。这里我们先给出测试的拷贝对象,大家可以拿这个一文带你详细了解JavaScript中的深拷贝对象来测试一下自己写的深拷贝函数是否完善:

// 测试的一文带你详细了解JavaScript中的深拷贝对象
const 一文带你详细了解JavaScript中的深拷贝 = {
    // =========== 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类型 ===========
    // 普通对象
    一文带你详细了解JavaScript中的深拷贝: {
        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(一文带你详细了解JavaScript中的深拷贝, 'innumerable', {
    enumerable: false,
    value: '不可枚举属性'
});

// 5.设置原型对象
Object.setPrototypeOf(一文带你详细了解JavaScript中的深拷贝, {
    proto: 'proto'
})

// 6.设置loop成循环引用的属性
一文带你详细了解JavaScript中的深拷贝.loop = 一文带你详细了解JavaScript中的深拷贝
登录后复制

一文带你详细了解JavaScript中的深拷贝对象在Chrome浏览器中的结果:

一文带你详细了解JavaScript中的深拷贝

2.1 浅拷贝

浅拷贝: 创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址所指向的对象,肯定会影响到另一个对象。

首先我们看看一些浅拷贝的方法(详细了解可点击对应方法的超链接):

方法 使用方式 注意事项
Object.assign() Object.assign(target, ...sources)
方法 使用方式 注意事项
Object.assign() Object.assign(target, ...sources)
说明:用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
1.不会拷贝对象的继承属性;
2.不会拷贝对象的不可枚举的属性;
3.可以拷贝 Symbol 类型的属性。
展开语法 let 一文带你详细了解JavaScript中的深拷贝Clone = { ...一文带你详细了解JavaScript中的深拷贝 }; 缺陷和Object.assign()差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。
Array.prototype.concat()拷贝数组 const new_array = old_array.concat(value1[, value2[, ...[, valueN]]]) 浅拷贝,适用于基本类型值的数组
Array.prototype.slice()拷贝数组 arr.slice([begin[, end]]) 浅拷贝,适用于基本类型值的数组
说明:用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
1.不会拷贝对象的继承属性; 2.不会拷贝对象的不可枚举的属性;3.可以拷贝 Symbol 类型的属性。
展开语法 let 一文带你详细了解JavaScript中的深拷贝Clone = { ...一文带你详细了解JavaScript中的深拷贝 }; 缺陷和Object.assign()差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。
Array.prototype.concat()拷贝数组 const new_array = old_array.concat(value1[, value2[, ...[, valueN]]]) 浅拷贝,适用于基本类型值的数组
Array.prototype.slice()拷贝数组 arr.slice([begin[, end]]) 浅拷贝,适用于基本类型值的数组

这里只列举了常用的几种方式,除此之外当然还有其他更多的方式。注意,我们直接使用=赋值不是浅拷贝,因为它是直接指向同一个对象了,并没有返回一个新对象。

手动实现一个浅拷贝:

function shallowClone(target) {
    if (typeof target === '一文带你详细了解JavaScript中的深拷贝ect' && target !== null) {
        const cloneTarget = Array.isArray(target) ? [] : {};
        for (let prop in target) {
            if (target.hasOwnProperty(prop)) {
                cloneTarget[prop] = target[prop];
            }
        }
        return cloneTarget;
    } else {
        return target;
    }
}


// 测试
const shallowCloneObj = shallowClone(一文带你详细了解JavaScript中的深拷贝)

shallowCloneObj === 一文带你详细了解JavaScript中的深拷贝  // false,返回的是一个新对象
shallowCloneObj.arr === 一文带你详细了解JavaScript中的深拷贝.arr  // true,对于对象类型只拷贝了引用
登录后复制

从上面这段代码可以看出,利用类型判断(查看typeof),针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性(for...in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性,包含原型上的属性。查看for…in),基本就可以手工实现一个浅拷贝的代码了。

2.2 深拷贝

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

看看现存的一些深拷贝的方法:

方法1:JSON.stringify()

JSON.stringfy() 其实就是将一个 JavaScript 对象或值转换为 JSON 字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象。(点这了解:JSON.stringfy()、JSON.parse())

使用如下:

function deepClone(target) {
    if (typeof target === '一文带你详细了解JavaScript中的深拷贝ect' && target !== null) {
        return JSON.parse(JSON.stringify(target));
    } else {
        return target;
    }
}

// 开头的测试一文带你详细了解JavaScript中的深拷贝存在BigInt类型、循环引用,JSON.stringfy()执行会报错,所以除去这两个条件进行测试
const clonedObj = deepClone(一文带你详细了解JavaScript中的深拷贝)

// 测试
clonedObj === 一文带你详细了解JavaScript中的深拷贝  // false,返回的是一个新对象
clonedObj.arr === 一文带你详细了解JavaScript中的深拷贝.arr  // false,说明拷贝的不是引用
登录后复制

浏览器执行结果:

一文带你详细了解JavaScript中的深拷贝
从以上结果我们可知JSON.stringfy() 存在以下一些问题:

  • 执行会报错:存在BigInt类型、循环引用。

  • 拷贝Date引用类型会变成字符串。

  • 键值会消失:对象的值中为FunctionUndefinedSymbol 这几种类型,。

  • 键值变成空对象:对象的值中为MapSetRegExp这几种类型。

  • 无法拷贝:不可枚举属性、对象的原型链。

  • 补充:其他更详细的内容请查看官方文档:JSON.stringify()

由于以上种种限制条件,JSON.stringfy() 方式仅限于深拷贝一些普通的对象,对于更复杂的数据类型,我们需要另寻他路。

方法2:递归基础版深拷贝

手动递归实现深拷贝,我们只需要完成以下2点即可:

  • 对于基础类型,我们只需要简单地赋值即可(使用=)。

  • 对于引用类型,我们需要创建新的对象,并通过遍历键来赋值对应的值,这个过程中如果遇到 Object 类型还需要再次进行遍历。

function deepClone(target) {
    if (typeof target === '一文带你详细了解JavaScript中的深拷贝ect' && target) {
        let cloneObj = {}
        for (const key in target) { // 遍历
            const val = target[key]
            if (typeof val === '一文带你详细了解JavaScript中的深拷贝ect' && val) {
                cloneObj[key] = deepClone(val) // 是对象就再次调用该函数递归
            } else {
                cloneObj[key] = val // 基本类型的话直接复制值
            }
        }
        return cloneObj
    } else {
        return target;
    }
}

// 开头的测试一文带你详细了解JavaScript中的深拷贝存在循环引用,除去这个条件进行测试
const clonedObj = deepClone(一文带你详细了解JavaScript中的深拷贝)

// 测试
clonedObj === 一文带你详细了解JavaScript中的深拷贝  // false,返回的是一个新对象
clonedObj.arr === 一文带你详细了解JavaScript中的深拷贝.arr  // false,说明拷贝的不是引用
登录后复制

浏览器执行结果:

一文带你详细了解JavaScript中的深拷贝
该基础版本存在许多问题:

  • 不能处理循环引用。

  • 只考虑了Object对象,而Array对象、Date对象、RegExp对象、Map对象、Set对象都变成了Object对象,且值也不正确。

  • 丢失了属性名为Symbol类型的属性。

  • 丢失了不可枚举的属性。

  • 原型上的属性也被添加到拷贝的对象中了。

如果存在循环引用的话,以上代码会导致无限递归,从而使得堆栈溢出。如下例子:

const a = {}
const b = {}
a.b = b
b.a = a
deepClone(a)
登录后复制

对象 a 的键 b 指向对象 b,对象 b 的键 a 指向对象 a,查看a对象,可以看到是无限循环的:
一文带你详细了解JavaScript中的深拷贝
对对象a执行深拷贝,会出现死循环,从而耗尽内存,进而报错:堆栈溢出
一文带你详细了解JavaScript中的深拷贝
如何避免这种情况呢?一种简单的方式就是把已添加的对象记录下来,这样下次碰到相同的对象引用时,直接指向记录中的对象即可。要实现这个记录功能,我们可以借助 ES6 推出的 WeakMap 对象,该对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。(WeakMap相关见这:WeakMap)

针对以上基础版深拷贝存在的缺陷,我们进一步去完善,实现一个完美的深拷贝

方法3:递归一文带你详细了解JavaScript中的深拷贝

对于基础版深拷贝存在的问题,我们一一改进:

存在的问题改进方案
1. 不能处理循环引用使用 WeakMap 作为一个Hash表来进行查询
2. 只考虑了Object对象当参数为 DateRegExpFunctionMapSet,则直接生成一个新的实例返回
3. 属性名为Symbol的属性
4. 丢失了不可枚举的属性
针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys()
Reflect.ownKeys(一文带你详细了解JavaScript中的深拷贝)相当于[...Object.getOwnPropertyNames(一文带你详细了解JavaScript中的深拷贝), ...Object.getOwnPropertySymbols(一文带你详细了解JavaScript中的深拷贝)]
4. 原型上的属性Object.getOwnPropertyDescriptors()设置属性描述对象,以及Object.create()方式继承原型链

代码实现:

function deepClone(target) {
    // WeakMap作为记录对象Hash表(用于防止循环引用)
    const map = new WeakMap()

    // 判断是否为一文带你详细了解JavaScript中的深拷贝ect类型的辅助函数,减少重复代码
    function isObject(target) {
        return (typeof target === '一文带你详细了解JavaScript中的深拷贝ect' && target ) || typeof target === 'function'
    }

    function clone(data) {

        // 基础类型直接返回值
        if (!isObject(data)) {
            return data
        }

        // 日期或者正则对象则直接构造一个新的对象返回
        if ([Date, RegExp].includes(data.constructor)) {
            return new data.constructor(data)
        }

        // 处理函数对象
        if (typeof data === 'function') {
            return new Function('return ' + data.toString())()
        }

        // 如果该对象已存在,则直接返回该对象
        const exist = map.get(data)
        if (exist) {
            return exist
        }

        // 处理Map对象
        if (data instanceof Map) {
            const result = new Map()
            map.set(data, result)
            data.forEach((val, key) => {
                // 注意:map中的值为一文带你详细了解JavaScript中的深拷贝ect的话也得深拷贝
                if (isObject(val)) {
                    result.set(key, clone(val))
                } else {
                    result.set(key, val)
                }
            })
            return result
        }

        // 处理Set对象
        if (data instanceof Set) {
            const result = new Set()
            map.set(data, result)
            data.forEach(val => {
                // 注意:set中的值为一文带你详细了解JavaScript中的深拷贝ect的话也得深拷贝
                if (isObject(val)) {
                    result.add(clone(val))
                } else {
                    result.add(val)
                }
            })
            return result
        }

        // 收集键名(考虑了以Symbol作为key以及不可枚举的属性)
        const keys = Reflect.ownKeys(data)
        // 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性以及对应的属性描述
        const allDesc = Object.getOwnPropertyDescriptors(data)
        // 结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链, 这里得到的result是对data的浅拷贝
        const result = Object.create(Object.getPrototypeOf(data), allDesc)

        // 新对象加入到map中,进行记录
        map.set(data, result)

        // Object.create()是浅拷贝,所以要判断并递归执行深拷贝
        keys.forEach(key => {
            const val = data[key]
            if (isObject(val)) {
                // 属性值为 对象类型 或 函数对象 的话也需要进行深拷贝
                result[key] = clone(val)
            } else {
                result[key] = val
            }
        })
        return result
    }

    return clone(target)
}



// 测试
const clonedObj = deepClone(一文带你详细了解JavaScript中的深拷贝)
clonedObj === 一文带你详细了解JavaScript中的深拷贝  // false,返回的是一个新对象
clonedObj.arr === 一文带你详细了解JavaScript中的深拷贝.arr  // false,说明拷贝的不是引用
clonedObj.func === 一文带你详细了解JavaScript中的深拷贝.func  // false,说明function也复制了一份
clonedObj.proto  // proto,可以取到原型的属性
登录后复制

详细的说明见代码中的注释,更多测试希望大家自己动手尝试验证一下以加深印象。

在遍历 Object 类型数据时,我们需要把 Symbol 类型的键名也考虑进来,所以不能通过 Object.keys 获取键名或 for...in 方式遍历,而是通过Reflect.ownKeys()获取所有自身的键名(getOwnPropertyNamesgetOwnPropertySymbols 函数将键名组合成数组也行:[...Object.getOwnPropertyNames(一文带你详细了解JavaScript中的深拷贝), ...Object.getOwnPropertySymbols(一文带你详细了解JavaScript中的深拷贝)]),然后再遍历递归,最终实现拷贝。

浏览器执行结果:
一文带你详细了解JavaScript中的深拷贝
可以发现我们的cloneObj对象和原来的一文带你详细了解JavaScript中的深拷贝对象一模一样,并且修改cloneObj对象的各个属性都不会对一文带你详细了解JavaScript中的深拷贝对象造成影响。其他的大家再多尝试体会哦!

【相关推荐:javascript视频教程编程视频

以上是一文带你详细了解JavaScript中的深拷贝的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1663
14
CakePHP 教程
1419
52
Laravel 教程
1313
25
PHP教程
1263
29
C# 教程
1236
24
如何使用WebSocket和JavaScript实现在线语音识别系统 如何使用WebSocket和JavaScript实现在线语音识别系统 Dec 17, 2023 pm 02:54 PM

如何使用WebSocket和JavaScript实现在线语音识别系统引言:随着科技的不断发展,语音识别技术已经成为了人工智能领域的重要组成部分。而基于WebSocket和JavaScript实现的在线语音识别系统,具备了低延迟、实时性和跨平台的特点,成为了一种被广泛应用的解决方案。本文将介绍如何使用WebSocket和JavaScript来实现在线语音识别系

WebSocket与JavaScript:实现实时监控系统的关键技术 WebSocket与JavaScript:实现实时监控系统的关键技术 Dec 17, 2023 pm 05:30 PM

WebSocket与JavaScript:实现实时监控系统的关键技术引言:随着互联网技术的快速发展,实时监控系统在各个领域中得到了广泛的应用。而实现实时监控的关键技术之一就是WebSocket与JavaScript的结合使用。本文将介绍WebSocket与JavaScript在实时监控系统中的应用,并给出代码示例,详细解释其实现原理。一、WebSocket技

如何利用JavaScript和WebSocket实现实时在线点餐系统 如何利用JavaScript和WebSocket实现实时在线点餐系统 Dec 17, 2023 pm 12:09 PM

如何利用JavaScript和WebSocket实现实时在线点餐系统介绍:随着互联网的普及和技术的进步,越来越多的餐厅开始提供在线点餐服务。为了实现实时在线点餐系统,我们可以利用JavaScript和WebSocket技术。WebSocket是一种基于TCP协议的全双工通信协议,可以实现客户端与服务器的实时双向通信。在实时在线点餐系统中,当用户选择菜品并下单

如何使用WebSocket和JavaScript实现在线预约系统 如何使用WebSocket和JavaScript实现在线预约系统 Dec 17, 2023 am 09:39 AM

如何使用WebSocket和JavaScript实现在线预约系统在当今数字化的时代,越来越多的业务和服务都需要提供在线预约功能。而实现一个高效、实时的在线预约系统是至关重要的。本文将介绍如何使用WebSocket和JavaScript来实现一个在线预约系统,并提供具体的代码示例。一、什么是WebSocketWebSocket是一种在单个TCP连接上进行全双工

JavaScript和WebSocket:打造高效的实时天气预报系统 JavaScript和WebSocket:打造高效的实时天气预报系统 Dec 17, 2023 pm 05:13 PM

JavaScript和WebSocket:打造高效的实时天气预报系统引言:如今,天气预报的准确性对于日常生活以及决策制定具有重要意义。随着技术的发展,我们可以通过实时获取天气数据来提供更准确可靠的天气预报。在本文中,我们将学习如何使用JavaScript和WebSocket技术,来构建一个高效的实时天气预报系统。本文将通过具体的代码示例来展示实现的过程。We

简易JavaScript教程:获取HTTP状态码的方法 简易JavaScript教程:获取HTTP状态码的方法 Jan 05, 2024 pm 06:08 PM

JavaScript教程:如何获取HTTP状态码,需要具体代码示例前言:在Web开发中,经常会涉及到与服务器进行数据交互的场景。在与服务器进行通信时,我们经常需要获取返回的HTTP状态码来判断操作是否成功,根据不同的状态码来进行相应的处理。本篇文章将教你如何使用JavaScript获取HTTP状态码,并提供一些实用的代码示例。使用XMLHttpRequest

javascript中如何使用insertBefore javascript中如何使用insertBefore Nov 24, 2023 am 11:56 AM

用法:在JavaScript中,insertBefore()方法用于在DOM树中插入一个新的节点。这个方法需要两个参数:要插入的新节点和参考节点(即新节点将要被插入的位置的节点)。

JavaScript和WebSocket:打造高效的实时图像处理系统 JavaScript和WebSocket:打造高效的实时图像处理系统 Dec 17, 2023 am 08:41 AM

JavaScript是一种广泛应用于Web开发的编程语言,而WebSocket则是一种用于实时通信的网络协议。结合二者的强大功能,我们可以打造一个高效的实时图像处理系统。本文将介绍如何利用JavaScript和WebSocket来实现这个系统,并提供具体的代码示例。首先,我们需要明确实时图像处理系统的需求和目标。假设我们有一个摄像头设备,可以采集实时的图像数

See all articles