網路上有很多關於深拷貝的文章,但是品質良莠不齊,有很多都考慮得不周到,寫的方法比較簡陋,難以令人滿意。本文旨在完成一個完美的深拷貝,大家看瞭如果有問題,歡迎一起補充完善。
評價一個深拷貝是否完善,請檢查以下問題是否都實現了:
#基本類型資料
是否能拷貝?鍵和值都是基本型別的
普通物件
是否能拷貝?Symbol
作為物件的key是否能拷貝?Date
和RegExp
物件類型是否能拷貝?Map
和Set
物件類型是否能拷貝?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中,基礎型別值的複製是直接拷貝一份新的一模一樣的數據,這兩份資料相互獨立,互不影響。而引用型別值(Object型別)的複製是傳遞物件的參考(也就是物件所在的記憶體位址,也就是指向物件的指標),就相當於多個變數指向同一個對象,那麼只要其中的一個變數對這個對象進行修改,其他的變數所指向的物件也會跟著修改(因為它們指向的是同一個物件)。如下圖:
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瀏覽器中的結果:
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,说明拷贝的不是引用
浏览器执行结果:
从以上结果我们可知JSON.stringfy()
存在以下一些问题:
执行会报错:存在
BigInt
类型、循环引用。拷贝
Date
引用类型会变成字符串。键值会消失:对象的值中为
Function
、Undefined
、Symbol
这几种类型,。键值变成空对象:对象的值中为
Map
、Set
、RegExp
这几种类型。无法拷贝:不可枚举属性、对象的原型链。
补充:其他更详细的内容请查看官方文档: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,说明拷贝的不是引用
浏览器执行结果:
该基础版本存在许多问题:
不能处理循环引用。
只考虑了
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
对象,可以看到是无限循环的:
对对象a
执行深拷贝,会出现死循环,从而耗尽内存,进而报错:堆栈溢出
如何避免这种情况呢?一种简单的方式就是把已添加的对象记录下来,这样下次碰到相同的对象引用时,直接指向记录中的对象即可。要实现这个记录功能,我们可以借助 ES6 推出的 WeakMap
对象,该对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。(WeakMap
相关见这:WeakMap)
针对以上基础版深拷贝存在的缺陷,我们进一步去完善,实现一个完美的深拷贝。
方法3:递归一文帶你詳細了解JavaScript中的深拷貝
对于基础版深拷贝存在的问题,我们一一改进:
存在的问题 | 改进方案 |
---|---|
1. 不能处理循环引用 | 使用 WeakMap 作为一个Hash表来进行查询 |
2. 只考虑了Object 对象 |
当参数为 Date 、RegExp 、Function 、Map 、Set ,则直接生成一个新的实例返回 |
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()
获取所有自身的键名(getOwnPropertyNames
和 getOwnPropertySymbols
函数将键名组合成数组也行:[...Object.getOwnPropertyNames(一文帶你詳細了解JavaScript中的深拷貝), ...Object.getOwnPropertySymbols(一文帶你詳細了解JavaScript中的深拷貝)]
),然后再遍历递归,最终实现拷贝。
浏览器执行结果:
可以发现我们的cloneObj对象
和原来的一文帶你詳細了解JavaScript中的深拷貝对象
一模一样,并且修改cloneObj对象
的各个属性都不会对一文帶你詳細了解JavaScript中的深拷貝对象
造成影响。其他的大家再多尝试体会哦!
【相关推荐:javascript视频教程、编程视频】
以上是一文帶你詳細了解JavaScript中的深拷貝的詳細內容。更多資訊請關注PHP中文網其他相關文章!

JavaScript在現實世界中的應用包括服務器端編程、移動應用開發和物聯網控制:1.通過Node.js實現服務器端編程,適用於高並發請求處理。 2.通過ReactNative進行移動應用開發,支持跨平台部署。 3.通過Johnny-Five庫用於物聯網設備控制,適用於硬件交互。

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

Python更适合数据科学和机器学习,JavaScript更适合前端和全栈开发。1.Python以简洁语法和丰富库生态著称,适用于数据分析和Web开发。2.JavaScript是前端开发核心,Node.js支持服务器端编程,适用于全栈开发。

JavaScript不需要安裝,因為它已內置於現代瀏覽器中。你只需文本編輯器和瀏覽器即可開始使用。 1)在瀏覽器環境中,通過標籤嵌入HTML文件中運行。 2)在Node.js環境中,下載並安裝Node.js後,通過命令行運行JavaScript文件。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

DVWA
Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

MinGW - Minimalist GNU for Windows
這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

Safe Exam Browser
Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

Atom編輯器mac版下載
最受歡迎的的開源編輯器

mPDF
mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),