6

The previous section on what constitutes JavaScript describes that the data types of JavaScript are divided into basic types and reference types, and the basis for distinguishing the two is that the basic type is "light", it exists in the stack memory, and the reference type is heavier, it exists in heap memory. Therefore, when the basic type is copied, it can be copied directly. When the reference type is copied, it is not the object (the reference type has and only one data type - object), but the address of the object in memory.

In everything is an object , we have clearly expressed a point of view, in the JavaScript world, except for undefined, null, everything is an object. One of the things that objects frequently use during use is assignment copying, and when you copy something wrong, things can go badly. So our section is born to explain the copy in the object

text

First of all, JavaScript does not have immutable data structures. Immutable data structures are necessary in functional programming.

The benefit of mutable is to save memory or do something with mutability, but in complex development its side effects are far greater than the benefits, so there are shallow and deep copies

The author has a few self-answered explanations here

  • Why is the object a copy address?

    • For performance (saving memory), imagine that if each object is a copy value, then the object is large/too many, and the memory occupied will increase geometrically
  • How to copy the value of an object

    • Object.assign
    • spread operator
    • slice (array method)
    • concat (array method)
    • JSON.stringify

These five methods can copy the value of the object, while the first four are shallow copies, and JSON.stringify is a deep copy

What is a shallow copy? What is a deep copy again?

  • A shallow copy creates a new object that replicates the original object's property values

    • The attribute is the basic type, the value of the basic type is copied, and the modified content does not affect
    • The attribute is a reference type, the copy is the memory address, and the modified content affects each other
  • Deep copy: The entire object is copied to another memory, and the modified content does not affect each other

To put it bluntly, a shallow copy only copies one layer, and a deep copy directly copies the object

Why should there be a shallow copy instead of a direct deep copy. Of course, this question was also raised by netizens - what is the role of JS's shallow copy? , Although the author did not find relevant information, I suspect that due to performance, shallow copy can cope with many scenarios, and deep copy is not necessary. In design, let developers use less, and virtually improve the development experience

Object.assign

The Object.assign() method can copy any number of enumerable properties of the original object to the target object, and then return the target object

It copies references to properties of the object, not the object itself

Is a new method of the Object object in ES6

parameter:

target: target object

sources: any number of original objects.

Return value: the target object will be returned

Applicable object: Object

Case number one:

 var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = Object.assign({}, obj1);
obj2.b = 100;
console.log(obj1); // {a: 10, b: 20, c: 30}
console.log(obj2); // {a: 10, b: 100, c: 30}

Case 2:

 var obj = { a: { a: 'hello', b: 21 } };
var initialObj = Object.assign({}, obj);

initialObj.a.a = 'changed';
console.log(obj.a.a); // "change"

It can be seen that Object can only copy the first layer of objects. If it is copied to a deeper layer, there will be a problem. So Object.assign is a shallow copy

spread operator (...)

Spread operator, which can expand array expressions or strings at the syntax level during function call/array construction; and expand object expressions by key-value when constructing literal objects

Granted, we all know that the spread operator is not meant for copying. But it is understandable, shallow copy is also one of the function points of the spread operator

Applicable objects: Object/Array

Case 1: One-dimensional array

 var arr = [1, 2, 3];
var arr2 = [...arr];
arr2.push(4);

// arr2 [1, 2, 3, 4]
// arr1 不受影响

Case 2: Multidimensional Arrays

 var a = [
    [1, 2],
    [3, 4],
    [5, 6],
];
var b = [...a];
b.shift().shift();
// b [[3, 4], [5, 6]]
// a [[2], [3, 4], [5, 6]]

Spread operator is also a shallow copy

slice

The slice() method returns a new array object that is a shallow copy of the original array determined by begin and end (including begin excluding --- end ). The original array will not be changed

Applicable object: Array

Case:

 const family = [
    'father',
    'mother',
    'brother',
    ['sister0', 'sister1', 'sister2'],
];
const copyFamily = family.slice();
copyFamily[0] = 'father1';
copyFamily[3][1] = 'brother1';
console.log(family); // ['father', 'mother', 'brother', ['sister0' , 'brother1', 'sister2']]
console.log(copyFamily); // ['father1', 'mother', 'brother', ['sister0' , 'brother1', 'sister2']]
// 复制一层,第二层开始引用

In the above case, the slice can only copy one layer, the second layer is to copy the reference address, and the slice is also a shallow copy

concat

The concat() method is used to combine two or more arrays. This method does not mutate the existing array, but returns a new array

Applicable object: Array

 const array1 = ['a', 'b', ['c0', 'c1', 'c2']];
const array2 = array1.concat();
array2[1] = 'B';
array2[2][1] = 'C1';
console.log(array1); // ['a', 'b', ['c0', 'C1', 'c2']]
console.log(array2); // ['a', 'B', ['c0', 'C1', 'c2']]
// 复制一层,第二层开始引用

concat is the same as slice, both are shallow copies of arrays

How to implement shallow copy

In simple terms, a shallow copy only copies the properties of one layer of objects

The role of hasOwnProperty is to determine whether the object itself has the specified property

 function shallowClone(source) {
    if (typeof target === 'object' && target !== null) {
        var target = Array.isArry(source) ? [] : {};
        for (let prop in source) {
            if (source.hasOwnProperty(prop)) {
                target[prop] = source[prop];
            }
        }
        return target;
    } else {
        return source;
    }
}

To sum up, there are 4 kinds of shallow copy in JavaScript. There are slice and concat for shallow copy of array, Object.assign() for object, and spread operator (...) for array and object.

The principle of deep copy

Shallow copy just creates a new object and copies the value of the basic type of the original object, while the reference type only copies one layer of properties, and the deeper one cannot be copied. The deep copy is different. It will open up a memory address in the heap memory and completely copy the original object.

Deep copy is to copy an object completely from memory to the target object, and open up a new space in heap memory to store the new object, and the modification of the new object will not change the meta object. separation

Simple induction: deep copy is to recursively copy the properties of objects in all levels

JSON.stringify

 var arr = [1, 2, 3, 4, { value: 5 }];
var arr1 = JSON.parse(JSON.stringify(arr));
arr[4].value = 6;
console.log(arr1); //[1, 2, 3, 4, { value: 5 }]

var obj = {
    name: "johan",
    address: {city: "shanghai"}
}
var obj1 = JSON.parse(JSON.stringify(obj));
obj.address.city = "beijing";
console.log(obj1); //{name: "johan", address:{city: "shanghai"}

Although JSON.stringify can achieve deep copying of arrays and objects, it has several pitfalls

  • It cannot clone special objects like functions, RegExp, etc.
  • It will discard the constructor of the object, all constructors will point to Object
  • The object has a circular reference, an error will be reported

Let's test these pits,

 // 构造函数
function Person(name) {
    this.name = name;
}

const Elaine = new Person('elaine');

// 函数
function say() {
    console.log('hi');
}

const oldObj = {
    a: say,
    b: new Array(1),
    c: new RegExp('ab+c', 'i'),
    d: Elaine,
};

const newObj = JSON.parse(JSON.stringify(oldObj));

// 无法复制函数
console.log(newObj.a, oldObj.a); // undefined [Function: say]
// 稀疏数组复制错误
console.log(newObj.b[0], oldObj.b[0]); // null undefined
// 无法复制正则对象
console.log(newObj.c, oldObj.c); // {} /ab+c/i
// 构造函数指向错误
console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: Object] [Function: person]

We can see in pairs of functions, regular objects. An accident will occur when cloning objects such as sparse arrays, and an error will occur when the constructor points to

 const oldObj = {};
oldObj.a = oldObj;

const newObj = JSON.parse(JSON.stringify(oldObj));
console.log(newObj.a, oldObj.a); // TypeError: Converting circular structure to JSON

Circular reference to object throws error

JSON.stringify deep copy can solve most scenarios in reality, but its flaws also make it a frequent visitor in interviews. Now we challenge handwritten deep copy

handwritten deep copy

The key to deep copy is recursion + deep copy

We first implement a deep copy for arrays and objects

 function deepClone(source) {
    // 针对基本数据类型
    if (typeof source !== 'object' || source === null ) {
        return source
    }
    // 判断它是数组还是对象
    // 或者 let target = source instanceof Array ? [] : {}
    let target = Array.isArray(source) ? [] : {}
    // 循环遍历复制每个属性
    for (let prop in source) {
         // 自有属性才做拷贝
        if (source.hasOwnProperty(prop)) {
           // 判断自有属性是否是对象
            target[prop] = typeof source[prop] === 'object' ? deepClone(source[prop]) : source[prop]
        }
    }
    return target
}

The above is a simple deep copy, which is similar to the deep copy effect of JSON.stringify. They also have the disadvantages of:

  • Executing this method on objects that contain circular references (objects refer to each other, forming an infinite loop) will throw an error
  • Attributes with the Symbol type as the attribute value will be ignored
  • Lack of compatibility with other built-in constructors such as Function, RegExp, Date, Set, Map

We use WeakMap to solve circular references, if you want other data types, add

Here's why we use WeakMap to solve circular references and how it differs from Map

To solve the circular reference problem, you can open up an additional storage space to store the corresponding relationship between the current object and the copied object. When copying, first look for it in the space, find it and return it directly, if not, copy it normally

And this kind of data structure can use map, WeakMap. The difference between the two is

  • A WeakMap object is a collection of key/value pairs, where the keys are weak references. Its keys must be objects, and the values can be arbitrary. Map keys can be arbitrary, including functions, objects, or any primitive type
  • WeakMaps are weak references and can be garbage collected. Map keys are bound to memory
  • Map can be traversed, WeakMap cannot be traversed

Simply put, because WeakMap is a weak reference, garbage collection can proceed normally when no other references exist.

 function deepClone(source, storage = new WeakMap()) {
    // 针对基本数据类型
    if (typeof source !== 'object' || source === null) {
        return source
    }
    // 是否是日期
    if (source.constructor === Date) {
        return new Date(source)
    }
    // 是否是正则
    if (source.constructor === RegExp) {
        return new RegExp(source)
    }
    // 是否是数组
    let target = source instanceof Array ? [] : {}
    // 循环引用 返回存储的引用数据
    if (storage.has(source)) return storage.get(source)
    // 开辟存储空间设置临时存储值
    storage.set(source, target)
    // 是否包含 Symbol 类型
    let isSymbol = Object.getOwnPropertySymbols(source)
    // 包含 Symbol 类型
    if (isSymbol.length) {
        isSymbol.forEach((item) => {
            if (typeof source[item] === 'object') {
                target[item] = deepClone(source[item], storage);
                return
            }
            target[item] = source[item]
        })
    }
    // 不包含 Symbol
    for(let key in source) {
        if (source.hasOwnProperty(key)) {
            target[key] = typeof source[key] === 'object' ? deepClone(sourcep[key], storage) : source[key]
        }
    }
    return target;
}

This deep copy of the author is definitely not the most complete, and the deep copy written by non-big guys that amazes the interviewer is comparable. The author can only say that it provides the idea of a deep copy

Summarize

Deep copying is a must in the front-end interview. If he asks you how to write by hand, if you only write JSON.parse(JSON.stringify(source)), you will definitely be unqualified. Writing hasOwnProperty can only stand on the sidelines, and if you solve the problems of circular reference, Symbol, copying of various data types, etc., it can show that you understand deep copying

References

series of articles


山头人汉波
394 声望555 粉丝