这是 JS 原生办法原理探索系列的第九篇文章。本文会介绍如何手写实现浅拷贝和深拷贝。
实现浅拷贝
什么是浅拷贝?
对原对象进行浅拷贝,会生成一个和它“一样”的新对象。然而这种拷贝只会拷贝原对象第一层的根本类型属性,援用类型属性依然和原对象共享同一个。
用一个简略的例子来了解:
let obj1 = { a: 'Jack', b: { c: 1 }}let obj2 = Object.assign({},obj1)obj1.a = 'Tom'obj1.b.c = 2console.log(obj1.a) // 'Tom'console.log(obj2.a) // 'Jack' console.log(obj1.b.c) // 2console.log(obj2.b.c) // 2
能够看到,因为新对象拷贝了原对象第一层的根本类型属性,所以批改 obj1.a
的值并不会影响 obj2.a
的值;同时,因为新对象和原对象共享同一个第一层的援用类型属性,所以批改 obj1.b
对象也会影响到 obj2.b
对象。
如何实现浅拷贝?
JS 中常见的浅拷贝办法有 Object.assign()
、...
开展运算符以及数组的 slice
办法。然而如果咱们要本人实现一个浅拷贝,应该怎么做呢?
其实也很简略,因为浅拷贝只作用在第一层,所以只须要遍历原对象,将它的每一个成员增加到新对象上就行。这里说的原对象指的是对象字面量、数组、类数组对象、Set 以及 Map 这些能够遍历的对象。对于其它的不可遍历对象以及根本类型的值间接将其返回即可。
代码如下:
function getType(obj){ return Object.prototype.toSrting.call(obj).slice(8,-1)}// 能够遍历的数据类型let iterableList = ['Object','Array','Arguments','Set','Map']// 浅拷贝function shallowCopy(obj){ let type = getType(obj) if(!iterableList.includes(type)) return obj let res = new obj.constructor() // 如果是 Set 或者 Map obj.forEach((value,key) => { type === 'Set' ? res.add(value) : res.set(key,value) }) // 如果是对象字面量、类数组对象或者数组 Reflect.ownKeys(obj).forEach(key => { res[key] = obj[key] }) return res}
一些要点:
- 初始化新对象
res
:获取原对象obj
的构造函数,用于创立一个和原对象同类型的实例 - 这里遍历对象或者数组有三种形式,第一种是应用
Reflect.ownKeys()
获取本身所有属性(无论是否能够枚举),第二种是应用for……in
+hasOwnProperty()
获取本身所有可枚举属性,第三种是应用Object.keys()
一次性获取本身所有可枚举属性
实现对象深拷贝
什么是深拷贝?
对原对象进行深拷贝,会生成一个和它“一样”的新对象。深拷贝会拷贝原对象所有层级上的根本类型属性和援用类型属性。还是通过一个例子了解一下:
let obj1 = { a: 'Jack', b: { c: 1 }}let obj2 = JSON.parse(JSON.stringify(obj1))obj1.a = 'Tom'obj1.b.c = 2console.log(obj1.a) // 'Tom'console.log(obj2.a) // 'Jack' console.log(obj1.b.c) // 2console.log(obj2.b.c) // 1
能够看到,无论对 obj1
作出什么批改,都不会影响到 obj2
,反之亦然,两者是齐全独立的。
如何实现深拷贝?
常见的实现深拷贝的形式是 JSON.parse(JSON.stringify())
。它能够应酬个别的深拷贝场景,然而也存在着不少问题,这些问题根本都是呈现在序列化的环节。
Date 类型的属性通过深拷贝之后会变成字符串:
let obj = { date : new Date()}JSON.parse(JSON.stringify(obj)) // {date: "2021-07-04T13:01:35.934Z"}
正则类型和谬误类型的属性通过深拷贝之后会变成空对象:
let obj = { reg : /\d+/gi, error : new Error()}JSON.parse(JSON.stringify(obj)) // {reg:{},error:{}}
如果 key 的 value 是函数类型、
undefined
类型、Symbol
类型,则通过深拷贝之后会失落:// 如果是对象,属性间接失落let obj = { fn: function(){}, name: undefined, sym: Symbol(), age: 12}JSON.parse(JSON.stringify(obj)) // {age:12}// 如果是数组,则变为 "null"let arr = [ function(){}, undefined, Symbol(), 12]JSON.parse(JSON.stringify(arr)) // ["null","null","null"12]
如果 key 是
Symbol
类型,则通过深拷贝之后会失落:let obj = {a:1}obj[Symbol()] = 2JSON.parse(JSON.stringify(obj)) // {a:1}
NaN
、Infinity
、-Infinity
通过深拷贝之后会变成 nulllet obj = { a:NaN, b:Infinity, c:-Infinity}JSON.parse(JSON.stringify(obj)) // {a:null,b:null,c:null}
可能导致
constructor
指向失落:function Super(){}let obj1 = new Super()let obj2 = JSON.parse(JSON.stringify(obj1))console.log(obj1.constructor) // Superconsole.log(obj2.constructor) // Object
JSON.stringify()
只能序列化对象本身的可枚举属性,而 constructor
并不是实例对象本身的属性,而是实例的原型对象的属性。因而在序列化实例对象 obj1 的时候,实际上并不会去解决 constructor
的指向,这样,它的指向就成为了默认的 Object。
存在循环援用的问题
let obj = {}obj.a = objJSON.parse(JSON.stringify(obj1))
下面的 obj 对象存在循环援用,也就是说,它是一个环状构造(非树状)的对象,这样的对象是无奈转化为 JSON 的,因而会报错:can't convert circular structure to JSON。
此外,咱们也能够思考应用 Lodash 提供的深拷贝办法。不过,如果要本人实现深拷贝,应该怎么做呢?咱们一步一步来看。
根底版本
深拷贝的外围其实就是==浅拷贝 + 递归==,不论层级嵌套有多深,咱们总能够通过一直的递归达到对象的最里层,实现根本类型属性以及不可遍历的援用类型属性的拷贝。
上面是最根底的深拷贝版本:
function deepClone(target){ if(typeof target === 'object'){ let cloneTarget = Array.isArray(target) ? []:{} Reflect.ownKeys(target).forEach(key => { cloneTarget[key] = deepClone(target[key]) }) return cloneTarget } else { return target }}
这里只思考数组和对象字面量的状况。依据初始传入的 target
是一个对象字面量还是数组,决定最终返回的 cloneTarget
是对象还是数组。接着遍历 target
的每一个本身属性,递归调用 deepClone
,如果属性曾经是根本类型,则间接返回;如果还是对象或者数组,就和初始的 target
进行一样的解决。最初,把解决好的后果一一增加给 cloneTarget
。
解决循环援用导致的爆栈问题
然而,这里存在一个循环援用的问题。
假如深拷贝的指标是上面这样的对象:
let obj = {}obj.a = obj
这样的对象,构造中存在回环,即存在循环援用:obj
通过属性 a 援用了本身,而 a 也肯定会有一个属性 a 再次援用本身 …… 最终会导致 obj
有限嵌套上来。而深拷贝的过程中因为用到了递归,有限嵌套的对象就会导致有限的递归,一直地压栈最终会导致堆栈溢出。
如何解决循环援用带来的爆栈问题呢?其实也很简略,只须要给递归创立一个进口即可。对于首次传入的对象或者数组,会用一个 WeakMap 记录以后指标和拷贝后果的映射关系,当检测到再次传入雷同的指标时,就不再进行反复的拷贝,而是间接从 WeakMap 中取出它对应的拷贝后果返回。
这里的“返回”其实就给递归创立了一个进口,因而不会有限递归上来,也就不会爆栈了。
因而改良后的代码如下:
function deepClone(target,map = new WeakMap()){ if(typeof target === 'object'){ let cloneTarget = Array.isArray(target) ? []:{} // 解决循环援用的问题 if(map.has(target)) return map.get(target) map.set(target,cloneTarget) Reflect.ownKeys(target).forEach(key => { cloneTarget[key] = deepClone(target[key],map) }) return cloneTarget } else { return target }}
解决其它数据类型
始终记住,咱们要解决的是三类指标:
- 根本数据类型:间接返回即可
- 能够持续遍历的援用数据类型:除了下面曾经解决的对象字面量和数组,还有类数组对象、Set、Map 。它们都属于能够持续遍历的、可能存在嵌套的援用类型,因而在解决的时候就须要递归
- 不能持续遍历的援用数据类型:包含函数、谬误对象、日期对象、正则对象、根本类型的包装对象(String、Boolean、Symbol、Number)等。它们是不能持续遍历的,或者说是“没有层级嵌套”的,因而再解决的时候须要拷贝一份一样的正本返回
1)类型判断函数
为了更好地判断是援用数据类型还是根本数据类型,能够应用一个 isObject
函数:
function isObject(o){ return o !== null && (typeof o === 'object' || typeof o === 'function')}
为了更精确地判断具体是什么数据类型,能够应用一个 getType
函数:
function getType(o){ return Object.prototype.toString.call(o).slice(8,-1)}// getType(1) "Number"// getType(null) "Null"
2)初始化函数
之前深拷贝对象字面量或者数组的时候,首先会将最终返回的后果 cloneTarget
初始化为 []
或者 {}
。同样地,对于 Set、Map 以及类数组对象,也须要进行雷同的操作,所以最好用一个函数对立实现 cloneTarget
的初始化。
function initCloneTarget(target){ return new target.constructor()}
通过 target.constructor
能够取得传进来的实例的构造函数,利用这个构造函数新创建一个同类型的实例并返回。
3)解决能够持续遍历的援用类型:类数组对象、Set、Map
类数组对象,其实和数组以及对象字面量的模式差不多,所以能够一块解决;解决 Set 和 Map 的流程也根本一样,然而不能采纳间接赋值的形式,而要应用 add
办法或者 set
办法,所以略微改良一下。
代码如下:
function deepClone(target,map = new WeakMap()){ // 如果是根本类型,间接返回即可 if(!isObject(target)) return target // 初始化返回后果 let type = getType(target) let cloneTarget = initCloneTarget(target) // 解决循环援用 if(map.has(target)) return map.get(target) map.set(target,cloneTarget) // 解决 Set if(type === 'Set'){ target.forEach(value => { cloneTarget.add(deepClone(value,map)) }) } // 解决 Map else if(type === 'Map'){ target.forEach((value,key) => { cloneTarget.set(key,deepClone(value,map)) }) } // 解决对象字面量、数组、类数组对象 else if(type === 'Object' || type === 'Array' || type === 'Arguments'){ Reflect.ownKeys(target).forEach(key => { cloneTarget[key] = deepClone(target[key],map) }) } return cloneTarget}
4)解决不能持续遍历的援用类型
当初来解决不能持续遍历的援用类型。对于这样的指标,咱们不能像根本数据类型那样间接返回,因为它们实质上也是对象,间接返回将返回雷同的援用,并没有达到拷贝的目标。正确的做法,应该是拷贝一份正本再返回。
如何拷贝呢?这里又分为两种状况。其中,String、Boolean、Number、谬误对象、日期对象都能够通过 new 的形式返回一个实例正本;而 Symbol、函数、正则对象的拷贝则无奈通过简略的 new 拷贝正本,须要独自解决。
拷贝 Symbol
function cloneSymbol(target){ return Object(target.valueOf()) // 或者 return Object(Symbol.prototype.valueOf.call(target)) // 或者 return Object(Symbol(target.description))}
PS:这里的 target
是 Symbol 根本类型的包装类型,调用 valueOf
能够取得它对应的拆箱后果,再把这个拆箱后果传给 Object,就能够结构原包装类型的正本了;为了保险起见,能够通过 Symbol 的原型调用 valueOf
;能够通过 .description
取得 symbol 的描述符,基于此也能够结构原包装类型的正本。
拷贝正则对象(参考 lodash 的做法)
function cloneReg(target) { const reFlags = /\w*$/; const result = new RegExp(target.source, reFlags.exec(target)); result.lastIndex = target.lastIndex; return result;}
拷贝函数(实际上函数没有必要拷贝)
function cloneFunction(target){ return eval(`(${target})`) // 或者 return new Function(`return (${target})()`)}
PS:传给 new Function 的参数申明了新创建的函数实例的函数体内容
接下来,用一个 directCloneTarget
函数解决以上所有状况:
function directCloneTarget(target,type){ let _constructor = target.constructor switch(type): case 'String': case 'Boolean': case 'Number': case 'Error': case 'Date': return new _constructor(target.valueOf()) // 或者 return new Object(_constructor.prototype.valueOf.call(target)) case 'RegExp': return cloneReg(target) case 'Symbol': return cloneSymbol(target) case 'Function': return cloneFunction(target) default: return null }
PS:留神这里有一些坑。
- 为什么应用
return new _constructor(target.valueOf())
而不是return new _constructor(target)
呢?因为如果传进来的target
是new Boolean(false)
,那么最终返回的实际上是new Boolean(new Boolean(false))
,因为参数并非空对象,因而它的值对应的不是冀望的 false,而是 true。所以,最好应用valueOf
取得包装类型对应的实在值。 - 也能够不应用根本类型对应的构造函数
_constructor
,而是间接new Object(target.valueOf())
对根本类型进行包装 - 思考到 valueOf 可能被重写,为了保险起见,能够通过根本类型对应的构造函数
_constructor
去调用 valueOf 办法
最终版本
最初的代码如下:
let objectToInit = ['Object','Array','Set','Map','Arguments']function deepClone(target,map = new WeakMap()){ if(!isObject(target)) return target // 初始化 let type = getType(target) let cloneTarget if(objectToInit.includes(type)){ cloneTarget = initCloneTarget(target) } else { return directCloneTarget(target,type) } // 解决循环援用 if(map.has(target)) return map.get(target) map.set(target,cloneTarget) // 拷贝 Set if(type === 'Set'){ target.forEach(value => { cloneTarget.add(deepClone(value,map)) }) } // 拷贝 Map else if(type === 'Map'){ target.forEach((value,key) => { cloneTarget.set(key,deepClone(value,map)) }) } // 拷贝对象字面量、数组、类数组对象 else if(type === 'Object' || type === 'Array' || type === 'Arguments'){ Reflect.ownKeys(target).forEach(key => { cloneTarget[key] = deepClone(target[key],map) }) } return cloneTarget}