目錄
深拷貝的最終實現
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資料類型的拷貝原理

先看看JS資料類型圖(除了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)
說明:用於將所有可列舉屬性的值從一個或多個來源物件指派到目標物件。它將傳回目標物件。
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)

如何使用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