What are deep and shallow copies?
From the previous article, we learned that when JavaScript is executed, the V8 engine stores variables used by the program in two different logical blocks of memory: the stack and the heap. As for why, simply put, the stack is the context in which the program is run, and the execution context can be switched very quickly through the stack. This requires that each element of the stack take up a fixed (32 bytes) amount of storage (for convenience, this fixed amount of space is referred to as a storage unit below), but we inevitably need to use some variables. It to store the value of the size of the occupied more than the size of a storage unit (of course, may also be the value of the initial size of no more than a storage unit, but it can be dynamically expanded to more than one storage unit), then put the need to store the value stored in the heap, and store it in the pile of address to the stack.
This solved the problem that a single storage unit (stack space) could not store large amounts of data, but it also created new problems. When we write code, we often need to copy a variable. For data whose value is stored on the stack, there is no problem with this operation. Copying its value is, as the name implies, copying a new one. For data with values stored on the heap, this creates an ambiguity, because the data entity of such data is stored in the heap space, and the stack space often keeps a shadow of the data entity, which can be traced by its shadow. So when you tell the JS engine that you want to copy such data, the JS engine does not know whether you want to copy a copy of its entity or a copy of its shadow. When we make a copy of its shadow, we call it a shallow copy, and when we make a copy of its body, we call it a deep copy. In simple terms, shallow copy copies the address of heap memory, while deep copy copies the value of heap memory.
JavaScript does not provide an operation or API called “copy”, just an assignment operator. The scenarios we use for assignment operators are nothing more than assigning a specific value to one variable or assigning the value of one variable to another variable, so since we are talking about copying, we are referring specifically to the second scenario. From the JS engine’s point of view, there are only two ways to copy data if it is stored in heap memory. We know that some primitive types of values (strings, symbols, etc.) are also stored in heap memory, but they do not have deep copies. In fact, there is only one way to copy all primitive values, no matter where they are stored.
A copy of a value of a primitive type
var num1 = 1
var num2 = num1
num1 = 2
console.log(num2) / / 1
var str1 = 'luwei'
var str2 = str1
console.log(str1[2]) // 'w'
str1[2] = 'W'
console.log(str1) // 'luwei'
console.log(str2) // 'luwei'
str1 = str1 + ' is 26 years old'
console.log(str1) // 'luwei is 26 years old'
console.log(str2) // 'luwei'
Copy the code
Here are two examples, both of which are basic types:
- However,
Number
The value of a type is stored on the stack, and its value is copied directly to the value, that is, a new block of stack memory is created to store andnum1
Same value. - But for
String
A type is stored in the same way that a value of a reference type is stored, its value is stored in heap memory, the stack memory just holds its reference, but it differs from an object in that there is no way to copy its value, a variablestr2
Only the and variables are saved instr1
In thestringluwei
A reference in heap memoryBut the oddity here is in the rightstr1
Unlike reference types, which can be used to modify heap memory, index assignments do not fail, but they do not work either. This is the immutability of strings. Once a string is generated, it is immutable, and once a string operation generates a new string, the JS engine simply allocates a heap to store the new string instead of modifying the original string
So for primitive types stored in stack space, we can only copy its value directly, not its address; For primitive types stored in heap memory, we can only copy their addresses, not their values. But no matter what the exact copy method is, there is only one way to copy values of primitive types, so there are no deep or shallow copies of values of primitive types.
As an aside, it is for this reason that many people understand the memory model as if all basic types are stored in stack space. Since we can’t use this address to directly change the value stored in the heap, once you change the value, the JS engine will return you a new address, so that it is no different from the value stored in the stack space.
How to copy a reference type
For a copy of a value of a reference type, we can either copy its value or copy its address. JS only provides an assignment operator to copy its address. JS does not provide a method for us to copy the value in the heap memory. The object. assign and expand operators we usually use seem to be able to copy objects, but they are also shallow copies of Object attributes.
To implement a deep copy of a reference type, we need to implement it ourselves. There are many ways to implement deep copy, some of them simple, and there are some problems associated with that. I’ll introduce two implementations. One is simpler, but very fast. The other is more complete and can be used in production.
The first:JSON.parse(JSON.stringify(object))
Parse (json.stringify (object)) the above code demonstrates a simple deep copy via json.parse (json.stringify (object)), but this approach has drawbacks:
let obj = {
reg : /^reg$/.fun: function(){},
syb: Symbol('foo'),
undefined: undefined
};
let copied_obj = JSON.parse(JSON.stringify(obj));
console.log(copied_obj); // { reg: {} }
Copy the code
- Ignores undefined
- Ignore the symbol
- Special objects such as function re objects cannot be serialized
- Cannot handle points to the same reference, the same reference will be copied repeatedly
let obj = {};
let obj2 = {name:'aaaaa'};
obj.ttt1 = obj2;
obj.ttt2 = obj2;
let cp = JSON.parse(JSON.stringify(obj));
obj.ttt1.name = 'change';
cp.ttt1.name = 'change';
// Obj ttt1 and ttt2 both refer to the same object, so if you change one of them, the other one will also change, i.e. obj.ttt1 === obj.ttt2
console.log(obj); // { ttt1: {name: "change"}, ttt2: {name: "change"}}
// Obj2 is copied twice in this way, missing cp.ttt1 === cp.ttt2
console.log(cp); // {ttt1: {name: "change"}, ttt2: {name: "aaaaa"}}
Copy the code
Second: recursive copy
function cloneDeep(value) {
let copied_objs = []; // Used to solve the circular reference problem
function _cloneDeep(value) {
if (value === null) return null;
if (typeof value === "object") {
// The value of the object type is first checked at copied_objs to see if it has occurred
for (let i = 0; i < copied_objs.length; i++) {
if (value === copied_objs[i].source) {
returncopied_objs[i].target; }}let new_value = {};
// We need to deal with arrays
if (Array.isArray(value)) new_value = [];
copied_objs.push({ source: value, target: new_value });
Object.keys(value).forEach((key) = > {
new_value[key] = _cloneDeep(value[key]);
});
return new_value;
} else {
returnvalue; }}return _cloneDeep(value);
}
Copy the code
The test case using LoDash passed, the related TypedArray use case did not pass, and the code above does not consider TypedArray compatibility
Side note: LoDash library each function of the test cases are very rich, in the learning of some important methods can be implemented by themselves and then use loDash test cases to test their own writing is not right, and then compare with the source code learning, I believe that can get twice the result with half the effort.