This is the ninth article in the series of exploring the principles of JS native methods. This article will introduce how to implement shallow copy and deep copy by hand.
Achieve shallow copy
What is a shallow copy?
A shallow copy of the original object will generate a new object "same" as it. But this kind of copy only copies the basic type attributes of the original object, and the reference type attributes still share the same one with the original object.
Use a simple example to understand:
let obj1 = {
a: 'Jack',
b: {
c: 1
}
}
let obj2 = Object.assign({},obj1)
obj1.a = 'Tom'
obj1.b.c = 2
console.log(obj1.a) // 'Tom'
console.log(obj2.a) // 'Jack'
console.log(obj1.b.c) // 2
console.log(obj2.b.c) // 2
It can be seen as a new object type attribute of the first copy of the base layer is the original object, thus changing obj1.a
value does not affect obj2.a
value; the same as the original object and the new object reference type attribute shared with a first layer , So modifying the obj1.b
object will also affect the obj2.b
object.
How to achieve shallow copy?
Common shallow copy methods in JS include Object.assign()
, ...
expansion operator and array slice
method. But if we want to implement a shallow copy ourselves, what should we do?
In fact, it is very simple, because the shallow copy only works on the first layer, so you only need to traverse the original object and add each of its members to the new object. The original object mentioned here refers to the traversable objects such as object literals, arrays, array-like objects, Set and Map. For other non-traversable objects and basic types of values, just return them directly.
code show as below:
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
}
Some key points:
- Initialize the new object
res
: Getobj
, which is used to create an instance of the same type as the original object - There are three ways to traverse objects or arrays. The first is to use
Reflect.ownKeys()
get all its attributes (whether it can be enumerated or not), the second is to usefor……in
+hasOwnProperty()
get all its enumerable attributes, and the third is to useObject.keys()
once Get all enumerable properties of itself
Implement deep copy of objects
What is a deep copy?
A deep copy of the original object will generate a new object "same" as it. Deep copy will copy the basic type attributes and reference type attributes on all levels of the original object. Or understand through an example:
let obj1 = {
a: 'Jack',
b: {
c: 1
}
}
let obj2 = JSON.parse(JSON.stringify(obj1))
obj1.a = 'Tom'
obj1.b.c = 2
console.log(obj1.a) // 'Tom'
console.log(obj2.a) // 'Jack'
console.log(obj1.b.c) // 2
console.log(obj2.b.c) // 1
We can see that both of obj1
make any changes will not affect the obj2
, and vice versa, the two are completely separate.
How to achieve deep copy?
The common way to implement deep copy is JSON.parse(JSON.stringify())
. It can cope with general deep copy scenarios, but there are also many problems, which basically appear in the serialization link.
The properties of the Date type will become strings after deep copying:
let obj = { date : new Date() } JSON.parse(JSON.stringify(obj)) // {date: "2021-07-04T13:01:35.934Z"}
The properties of regular type and error type will become empty objects after deep copying:
let obj = { reg : /\d+/gi, error : new Error() } JSON.parse(JSON.stringify(obj)) // {reg:{},error:{}}
If the value of the key is a function type,
undefined
type, orSymbol
type, it will be lost after deep copying:// 如果是对象,属性直接丢失 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]
If the key is of
Symbol
, it will be lost after deep copying:let obj = {a:1} obj[Symbol()] = 2 JSON.parse(JSON.stringify(obj)) // {a:1}
NaN
,Infinity
,-Infinity
will become null after deep copylet obj = { a:NaN, b:Infinity, c:-Infinity } JSON.parse(JSON.stringify(obj)) // {a:null,b:null,c:null}
May lead to the loss of
constructor
function Super(){} let obj1 = new Super() let obj2 = JSON.parse(JSON.stringify(obj1)) console.log(obj1.constructor) // Super console.log(obj2.constructor) // Object
JSON.stringify()
can only serialize the enumerable properties of the object itself, and constructor
not the property of the instance object itself, but the property of the prototype object of the instance. Therefore, when the instance object obj1 is constructor
, the point of 06107a03517c2f is not actually processed. In this way, its point becomes the default Object.
There is a circular reference problem
let obj = {} obj.a = obj JSON.parse(JSON.stringify(obj1))
The above obj object has a circular reference, that is, it is a circular structure (non-tree) object. Such an object cannot be converted into JSON, so an error will be reported: can't convert circular structure to JSON.
In addition, we can also consider using the deep copy method provided by Lodash. However, if you want to implement deep copy yourself, what should you do? Let's look at it step by step.
Basic version
The core of deep copy is actually == shallow copy + recursion ==. No matter how deep the nesting is, we can always reach the innermost layer of the object through continuous recursion to complete the copy of basic type attributes and non-traversable reference type attributes. .
The following is the most basic deep copy version:
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
}
}
Here only consider the case of arrays and object literals. According to whether the initially incoming target
is an object literal or an array, it is determined whether the final cloneTarget
returned is an object or an array. Then traversing target
each own properties, recursive calls deepClone
, if the attribute has a basic type, directly returns; or if the object or the array, and on the initial target
performed in the same process. Finally, add the processed results to cloneTarget
one by one.
Solve the problem of stack explosion caused by circular references
However, there is a circular reference problem.
Assume that the target of deep copy is the following object:
let obj = {}
obj.a = obj
For such an object, there is a loop in the structure, that is, there is a circular reference: obj
refers to itself through the attribute a, and a must also have an attribute a that references itself again... It will eventually cause obj
nest infinitely. In the process of deep copy, because of the use of recursion, infinitely nested objects will lead to infinite recursion, and continuous push on the stack will eventually lead to stack overflow.
How to solve the problem of stack explosion caused by circular references? In fact, it is very simple, only needs to create an exit for recursion to . For the object or array passed in for the first time, a WeakMap will be used to record the mapping relationship between the current target and the copy result. When the same target is detected again, the repeated copy will not be performed, but it will be taken out directly from the WeakMap The corresponding copy result is returned.
The "return" here actually creates an exit for the recursion, so it will not recurse indefinitely, and the stack will not burst.
Therefore, the improved code is as follows:
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
}
}
Processing other data types
Always remember that we are dealing with three types of goals:
- Basic data type: just return directly
- Reference data types that can continue to be traversed: In addition to the object literals and arrays that have been processed above, there are also array-like objects, Set, and Map. They all belong to the reference type that can be traversed continuously, and there may be nested reference types, so recursion is required when processing
- Reference data types that cannot be traversed: include functions, error objects, date objects, regular objects, basic types of packaging objects (String, Boolean, Symbol, Number), etc. They cannot continue to be traversed, or "no hierarchical nesting", so you need to make a copy of the same copy when you process it again.
1) Type judgment function
In order to better judge whether it is a reference data type or a basic data type, you can use a isObject
function:
function isObject(o){
return o !== null && (typeof o === 'object' || typeof o === 'function')
}
In order to more accurately determine the specific data type, you can use a getType
function:
function getType(o){
return Object.prototype.toString.call(o).slice(8,-1)
}
// getType(1) "Number"
// getType(null) "Null"
2) Initialization function
When deep copying object literals or arrays before, first initialize the cloneTarget
[]
or {}
. Similarly, for Set, Map, and array-like objects, the same operations need to be performed, so it is best to use one function to uniformly initialize cloneTarget
function initCloneTarget(target){
return new target.constructor()
}
Through target.constructor
can get the constructor of the passed in instance, use this constructor to create a new instance of the same type and return.
3) Processing reference types that can be traversed continuously: array-like objects, Set, Map
Array-like objects, in fact, are similar in form to arrays and object literals, so they can be processed together; the process of processing Set and Map is basically the same, but direct assignment cannot be used, but the add
method or the set
method should be used, so slightly improved a bit.
code show as below:
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) Deal with reference types that cannot be traversed
Now let's deal with reference types that cannot be traversed any further. For such a goal, we cannot return directly like basic data types, because they are also objects in nature, and returning directly will return the same reference, which does not achieve the purpose of copying. The correct way is to make a copy and then return it.
How to copy it? There are two situations. Among them, String, Boolean, Number, error object, and date object can all return a copy of an instance through new; while the copy of Symbol, function, and regular object cannot be copied through simple new and needs to be processed separately.
Copy Symbol
function cloneSymbol(target){
return Object(target.valueOf())
// 或者
return Object(Symbol.prototype.valueOf.call(target))
// 或者
return Object(Symbol(target.description))
}
PS: target
here is the packaging type valueOf
get its corresponding unboxing result, and then pass the unboxing result to Object to construct a copy of the original packaging type; to be safe, you can pass The prototype of Symbol calls valueOf
; the descriptor of the symbol can be obtained through .description
, and a copy of the original packaging type can also be constructed based on this.
Copy regular objects (refer to lodash's approach)
function cloneReg(target) {
const reFlags = /\w*$/;
const result = new RegExp(target.source, reFlags.exec(target));
result.lastIndex = target.lastIndex;
return result;
}
Copy function (actually the function does not need to be copied)
function cloneFunction(target){
return eval(`(${target})`)
// 或者
return new Function(`return (${target})()`)
}
PS: The parameter passed to new Function declares the function body content of the newly created function instance
Next, use a directCloneTarget
handle all the above cases:
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: Note that there are some pits here.
- Why use
return new _constructor(target.valueOf())
instead ofreturn new _constructor(target)
? Because if the incomingtarget
isnew Boolean(false)
, then the final return is actuallynew Boolean(new Boolean(false))
. Since the parameter is not an empty object, its value corresponds to true instead of the expected false. Therefore, it is best to usevalueOf
obtain the true value corresponding to the package type. - It is also possible not to use the constructor
_constructor
corresponding to the basic type, but to directlynew Object(target.valueOf())
the basic type with 06107a035183c7 - Considering that valueOf may be rewritten, to be on the safe side, you can call the valueOf method
_constructor
Final version
The final code is as follows:
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
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。