preface

The realization of a deep clone is a common question in the interview, but the overwhelming majority of the answer of the interviewer is incomplete, or even wrong, this time the interviewer will continue to ask, to see if you understand the principle of deep clone, in many cases, some of the interviewees with only a little knowledge of the original form of the leak.

Let’s take a look at how to achieve a deep clone, of course, the interview did not let you complete the realization of time, but you must be clear about where the pit, can easily deal with the interviewer’s follow-up.

Before we can implement a deep clone we need to look at the basic types in javascript.

Javascript Base types

JavaScript primitive types :Undefined, Null, Boolean, Number, String, Symbol

JavaScript reference type :Object


1. The shallow clone

Shallow clones are called shallow clones because objects are only cloned at the most external level, and further objects are still referenced to the same heap memory.

// The shallow clone function
function shallowClone(o) {
  const obj = {};
  for ( let i in o) {
    obj[i] = o[i];
  }
  return obj;
}
// The cloned object
const oldObj = {
  a: 1.b: [ 'e'.'f'.'g'].c: { h: { i: 2}}};const newObj = shallowClone(oldObj);
console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // true

Copy the code

As you can see, although oldobj.c.h is cloned, it is still equal to oldobj.c.h, indicating that they are still pointing to the same heap memory. This makes it a bad clone if you change newobj.c.h and affect oldobj.c.h.

newObj.c.h.i = 'change';
console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 'change' }
Copy the code

We changed the value of newobj.c.H.i. and oldobj.C.H.I. was also changed. That’s the problem with shallow clones.

Of course, a new apiObject.assign() can also do shallow copying, but the effect is the same, so we won’t go into details.

2. Deep cloning

2.1 JSON parse method

A few years ago, a legend circulated on Weibo that the most convenient way to achieve deep cloning. The PARSE method of JSON object can deserialize JSON strings into JS objects, and the Stringify method can serialize JS objects into JSON strings. The combination of these two methods can produce a convenient deep clone.

const newObj = JSON.parse(JSON.stringify(oldObj));
Copy the code

Let’s test this again with the example from the previous section

const oldObj = {
  a: 1.b: [ 'e'.'f'.'g'].c: { h: { i: 2}}};const newObj = JSON.parse(JSON.stringify(oldObj));
console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // false
newObj.c.h.i = 'change';
console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 2 }

Copy the code

Sure enough, this is a great way to achieve deep cloning, but is the solution too simple?

Indeed, this approach solves most usage scenarios, but it has many pitfalls.

1. It cannot clone special objects such as functions and RegExp

2. Discard constructor for objects; all constructors will point to objects

3. The object has a circular reference, causing an error

The main pit is the above points, let’s test one by one.

// The constructor
function person(pname) {
  this.name = pname;
}

const Messi = new person('Messi');

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

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

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

// The function cannot be copied
console.log(newObj.a, oldObj.a); // undefined [Function: say]
// Sparse array copy error
console.log(newObj.b[0], oldObj.b[0]); // null undefined
// Unable to copy the regular object
console.log(newObj.c, oldObj.c); // {} /ab+c/i
// The constructor points to an error
console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: Object] [Function: person]

Copy the code

We can see that accidents occur when cloning objects such as functions, regular objects, sparse arrays, and constructor Pointers.

const oldObj = {};

oldObj.a = oldObj;

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

Object will throw an error.

2.2 Construct a deep clone function

We know that the sequence/dissequence mentioned in the previous section is not possible to achieve a reliable deep cloning method, and that the methods mentioned in the tutorial are generally unreliable, and their problems are the same as those highlighted in the previous sequence and dissequence operation.

(This approach also presents the same problems as in the previous section.)

Because different objects (re, array, Date, etc.) need to be treated differently, we need to implement an object type judgment function.

const isType = (obj, type) = > {
  if (typeofobj ! = ='object') return false;
  const typeString = Object.prototype.toString.call(obj);
  let flag;
  switch (type) {
    case 'Array':
      flag = typeString === '[object Array]';
      break;
    case 'Date':
      flag = typeString === '[object Date]';
      break;
    case 'RegExp':
      flag = typeString === '[object RegExp]';
      break;
    default:
      flag = false;
  }
  return flag;
};
Copy the code

In this way, we can determine the type of special objects, so that we can use targeted cloning strategy.

const arr = Array.of(3.4.5.2);

console.log(isType(arr, 'Array')); // true
Copy the code

With regular objects, we need to add some new knowledge before we can deal with them.

We need to know about the flags attribute and so on through regular extensions, so we need to implement a function to extract the flags.

const getRegExp = re= > {
  var flags = ' ';
  if (re.global) flags += 'g';
  if (re.ignoreCase) flags += 'i';
  if (re.multiline) flags += 'm';
  return flags;
};
Copy the code

With these preparations in place, we can proceed with the implementation of deep cloning.

/** * deep Clone * @param {[type]} Parent Object Object to be cloned * @return {[type]} Deep clone object */
const clone = parent= > {
  // Maintain two arrays that store circular references
  const parents = [];
  const children = [];

  const _clone = parent= > {
    if (parent === null) return null;
    if (typeofparent ! = ='object') return parent;

    let child, proto;

    if (isType(parent, 'Array')) {
      // Do something special to the array
      child = [];
    } else if (isType(parent, 'RegExp')) {
      // Do special treatment for regular objects
      child = new RegExp(parent.source, getRegExp(parent));
      if (parent.lastIndex) child.lastIndex = parent.lastIndex;
    } else if (isType(parent, 'Date')) {
      // Do something special for the Date object
      child = new Date(parent.getTime());
    } else {
      // Handle the object prototype
      proto = Object.getPrototypeOf(parent);
      // Break the prototype chain with object.create
      child = Object.create(proto);
    }

    // Handle circular references
    const index = parents.indexOf(parent);

    if(index ! =- 1) {
      // If the parent array has this object, it means it has been referenced before, so return this object directly
      return children[index];
    }
    parents.push(parent);
    children.push(child);

    for (let i in parent) {
      / / recursion
      child[i] = _clone(parent[i]);
    }

    return child;
  };
  return _clone(parent);
};
Copy the code

Let’s do a test

function person(pname) {
  this.name = pname;
}

const Messi = new person('Messi');

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

const oldObj = {
  a: say,
  c: new RegExp('ab+c'.'i'),
  d: Messi,
};

oldObj.b = oldObj;


const newObj = clone(oldObj);
console.log(newObj.a, oldObj.a); // [Function: say] [Function: say]
console.log(newObj.b, oldObj.b); // { a: [Function: say], c: /ab+c/i, d: person { name: 'Messi' }, b: [Circular] } { a: [Function: say], c: /ab+c/i, d: person { name: 'Messi' }, b: [Circular] }
console.log(newObj.c, oldObj.c); // /ab+c/i /ab+c/i
console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: person] [Function: person]
Copy the code

Our deep cloning is not perfect, of course, such as Buffer object, Promise, Set, Map may be we need to do special processing, in addition to ensuring that no circular reference object, we can save the circular reference special processing, because it is very time consuming, but a basic deep cloning function we have achieved.


conclusion

Implementing a full deep clone is a lot of work to do, and some library implementations on NPM are not complete enough, so lodash’s deep clone is best implemented in a production environment.

We mentioned in the interview process, many of the pit is the interviewer is likely to ask you, want to know where the pit, can answer is your pluses, during the interview process must have one or two bright spots, if only know the sequence/reverse sequence this opportunistic approach, in asking, not only can’t get points is likely to give the impression that the only know a fur, after all, the interview will face It’s the depth of your knowledge.