Don’t you know how to handwrite dark and light copies? (Latest in 2023)

Preface

In JavaScript programming, we often need to copy data. So when to use deep copy and when to use shallow copy? This is worth thinking about.


Shallow copy


Shallow copy is a copy of the top-level structure of an object. It creates a new object, but this new object only copies the heap memory reference of the original object, not the object itself. Shallow copy will cause the new object and the original object to share the same heap memory. That is to say, if the attribute value of the original object changes, the attribute value of the new object will also change, and vice versa.

object.assign 

object.assign is a method of object in ES6. This method can be used for many purposes such as merging JS objects. One of the uses is to perform shallow copying. The first parameter of this method is the target object to be copied, and the following parameters are the source objects to be copied (it can also be multiple sources).

The syntax of object.assign is: Object.assign(target, ...sources)

shortcoming 

  • Inherited properties of objects are not copied
  • Non-enumerable properties of objects are not copied
let obj1 = { a:{ b:1 }, sym:Symbol(1)}; 
Object.defineProperty(obj1, 'innumerable' ,{
    value:'不可枚举属性',
    enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1);
console.log('obj2',obj2);

 

As you can see from the above code, you can also use object.assign to copy Symbol type objects, but if you get to the object's second-layer attribute obj1.ab, the change in the former's value will also affect the latter's second-layer property. The value of the attribute shows that there is still a problem of accessing the common heap memory, which means that this method cannot further copy, but only completes the shallow copy function.

spread operator

Utilize JavaScript's spread operator to complete the shallow copy function while constructing objects. 

扩展运算符的语法为:`let cloneObj = { ...obj };`
/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj)  //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj)  //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果

The spread operator has the same flaw as object.assign, that is, the implemented shallow copy function is similar. However, if the attributes are all basic type values, it will be more convenient to use the spread operator to perform shallow copy.

concat copies array 

The array  concat method is actually a shallow copy, so when connecting an array containing a reference type, you need to pay attention to modifying the attributes of the elements in the original array, because it will affect the array connected after copying. However,  concat it can only be used for shallow copies of arrays, and the usage scenarios are relatively limited. The code is shown below. 

let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);  // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]

 slice copy array

slice The method is also quite limited because 它仅仅针对数组类型. slice方法会返回一个新的数组对象, this object determines the start and end time of the original array interception by the first two parameters of the method, and will not affect or change the original array.

slice 的语法为:arr.slice(begin, end);
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);  //[ 1, 2, { val: 1000 } ]

As you can see from the code above, this is it 浅拷贝的限制所在了——它只能拷贝一层对象. if 存在对象的嵌套,那么浅拷贝将无能为力. Therefore, deep copy was born to solve this problem. It can solve the problem of multi-layer object nesting and completely realize copying.

Ideas        

  1. Make a basic copy of the base type
  2. Create a new storage for the reference data type and copy a layer of object attributes (and still point to the same memory space as the original object)

accomplish 


const shallowClone = (target) => {
  // 判断目标对象是否为对象类型且不为null
  if (typeof target === 'object' && target !== null) {
    // 创建一个克隆对象 cloneTarget,如果目标对象是数组则创建一个空数组,否则创建一个空对象
    const cloneTarget = Array.isArray(target) ? []: {};
    // 遍历目标对象的属性,使用 for...in 循环,判断属性是否为对象本身的属性(而非原型链上的属性)
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
        // 给克隆对象的属性赋值,将目标对象的属性值赋给克隆对象的属性
        cloneTarget[prop] = target[prop];
      }
    }
    // 返回克隆对象
    return cloneTarget;
  } else {
    // 如果目标对象不是对象类型或为null,则直接返回目标对象
    return target;
  }
}

 

 By using type judgment and performing a for loop for reference type objects to traverse the object properties and assign values ​​to the properties of the target object, you can basically implement a shallow copy of the code manually.

Deep copy principle and implementation

Shallow copy only creates a new object and copies the basic type value of the original object, while the reference data type only copies one layer of attributes, and deeper copies cannot be made. Deep copy is different. For complex reference data types, it completely opens up a memory address in the heap memory and completely copies the original object for storage.

These two objects are independent of each other and unaffected, completely achieving memory separation. In general, the principle of deep copy can be summarized as follows:

A complete copy of an object from the memory is given to the target object, and a new space is opened up in the heap memory to store the new object, and modifications to the new object will not change the original object, achieving true separation between the two. 

Beggar’s version of JSON.stringify() 

JSON.stringify() It is the simplest deep copy method in the current development process. It actually serializes an object into a  JSON string, converts the content in the object into a string, and finally uses  JSON.parse() the method to  JSON generate a new object from the string. 

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

 shortcoming

  • If the value of the copied object has any 函数of these types, the key-value pair will disappear in the string after JSON.stringify() sequence undefined.symbol
  • Copy Date引用类型will become a string
  • Cannot copy non-enumerable properties
  • Unable to copy object's prototype chain
  • Copy RegExp引用类型will become an empty object
  • The object contains NaN, Infinityand -Infinitythe result of JSON.parse() serialization will becomenull
  • The circular reference of the object cannot be copied, which is 对象成环(obj[key]=obj)the case of
function Obj() { 
  this.func = function () { alert(1) }; 
  this.obj = {a:1};
  this.arr = [1,2,3];
  this.und = undefined; 
  this.reg = /123/; 
  this.date = new Date(0); 
  this.NaN = NaN;
  this.infinity = Infinity;
  this.sym = Symbol(1);
} 
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{ 
  enumerable:false,
  value:'innumerable'
});
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);

 

 

Although he still has the above problems, it is enough to meet daily development needs, simple and fast

Use the JSON.stringify method to implement deep copy objects. Although there are still many functions that cannot be implemented so far, this method is sufficient to meet daily development needs and is the simplest and fastest. For other data types corresponding to more troublesome attributes that need to be deep copied, JSON.stringify is still not satisfactory for the time being, so the following methods are needed.

Basic version (handwritten recursive implementation)

The following is an example of implementing deepClone function encapsulation. It uses for in to traverse the attribute values ​​​​of the incoming parameters. If the value is a reference type, the function is called recursively again. If it is a basic data type, it is copied directly.

let obj1 = {
  a:{
    b:1
  }
}
function deepClone(obj) { 
  let cloneObj = {}
  for(let key in obj) {                 //遍历
    if(typeof obj[key] ==='object') { 
      cloneObj[key] = deepClone(obj[key])  //是对象就再次调用该函数递归
    } else {
      cloneObj[key] = obj[key]  //基本类型的话直接复制值
    }
  }
  return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2);   //  {a:{b:1}}

 

 Although recursion can be used to implement a deep copy, like JSON.stringify above, there are still some problems that have not been completely solved, such as:

  1. This deep copy function cannot copy non-enumerable properties and Symbol types;
  2. This method only performs recursive copying of values ​​of ordinary reference types, but cannot copy them correctly for reference types such as Array, Date, RegExp, Error, and Function;
  3. There is a loop in the object's properties, that is, the circular reference is not resolved.

This basic version is also relatively simple to write and can handle most application situations. But there are still many flaws, so what should we do? ,

Don’t worry, we also have advanced versions

Advanced version (recursive implementation)

What should we do to solve the above problems?

  1. For the non-enumerable properties and Symbol types that can traverse the object, we can use the Reflect.ownKeys (can get all properties of an object, including enumerable properties, non-enumerable properties and Symbol type properties) method
  2. When the parameter is of Date or Regexp type, a new instance is directly generated and returned.
  3. Use the Object.getOwnPropertyDescriptors method to get all the properties of the object and the corresponding characteristics. By the way, combine it with the Object.create method to create a new object and inherit the prototype chain of the original object.
  4. Use the weakMap type as a Hash table. Because weakMap is a weak reference type, it can effectively prevent memory leaks. It is very helpful for detecting circular references. If there is a cycle, the reference directly returns the value stored in the weakmap.

Implement deep copy 

// 判断一个对象是否为复杂数据类型,即对象或函数类型,且不为 null
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)

// 定义深拷贝函数 deepClone,接受两个参数:obj 为要进行深拷贝的目标对象,hash 为已经拷贝过的对象的哈希表(用于解决循环引用问题)
const deepClone = function (obj, hash = new WeakMap()) {
  // 如果目标对象是日期对象,则直接返回一个新的日期对象,避免修改原日期对象
  if (obj.constructor === Date) {
    return new Date(obj)
  }
  
  // 如果目标对象是正则对象,则直接返回一个新的正则对象,避免修改原正则对象
  if (obj.constructor === RegExp){
    return new RegExp(obj)
  }
  
  // 如果目标对象已经被拷贝过,则从 hash 中获取已经拷贝过的对象并返回,避免出现循环引用问题
  if (hash.has(obj)) {
    return hash.get(obj)
  }

  // 获取目标对象的所有属性描述符
  let allDesc = Object.getOwnPropertyDescriptors(obj)

  // 创建一个新对象 cloneObj,并将其原型链指向 obj 的原型对象
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)

  // 将目标对象和克隆对象的映射关系存入 hash 中,用于解决循环引用问题
  hash.set(obj, cloneObj)

  // 遍历目标对象的所有属性(包括字符串类型和 Symbol 类型的属性名)
  for (let key of Reflect.ownKeys(obj)) { 
    // 如果目标对象的属性值是复杂数据类型(即对象或数组),则递归调用 deepClone 函数进行深拷贝,并将拷贝结果赋值给克隆对象的对应属性
    if (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') {
      cloneObj[key] = deepClone(obj[key], hash)
    } else {
      // 如果目标对象的属性值不是复杂数据类型,则直接将其赋值给克隆对象的对应属性
      cloneObj[key] = obj[key]
    }
  }
  // 返回深拷贝后的新对象
  return cloneObj
}


// 下面是验证代码
let obj = {
  num: 0,
  str: '',
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: '我是一个对象', id: 1 },
  arr: [0, 1, 2],
  func: function () { console.log('我是一个函数') },
  date: new Date(0),
  reg: new RegExp('/我是一个正则/ig'),
  [Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
  enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj    // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)
 

Summarize

In this article, we introduce dark and shallow copying and their respective advantages and disadvantages. Then, from the shallower to the deeper, we explain the ideas and implementation of the handwritten dark and shallow copying method step by step. This is very helpful for us to deeply understand the underlying principles of js.

Finally, I wish everyone becomes stronger! ! ! nb

Guess you like

Origin blog.csdn.net/YN2000609/article/details/132410435