As we all know, the JS language itself does not provide deep copy of the Object function, whether it is direct assignment, object.assign, expansion operators… All are shallow copies. Some concepts about deep and shallow copies of JS can be referred to an article I wrote a long time ago

There are many excellent articles and implementations on the web about how to implement deep copy. This article focuses on an unconventional way to implement deep copy using non-recursive methods

In this article, only three data types are considered: array, object, and simple value

To realize the determination of data types, first to implement the three tool methods to determine the type. The most common is to use the Object. The prototype. The characteristics of the toString

const getType = v= > {
    switch (Object.prototype.toString.call(v)) {
        case "[object Object]":
            return "Object";
        case "[object Array]":
            return "Array";
        default:
            // Consider only arrays and objects, the rest are simple values
            return false; }};Copy the code

The recursive implementation

Before I talk about the non-recursive implementation, let’s take a look at the deep copy implementation of the recursive version, which is very simple, directly into the code

const copy = source= > {
    const _cp = source= > {
        let dest;
        const type = getType(source);
        if (type === "Array") {
            dest = [];
            source.forEach((item, index) = > {
                dest[index] = _cp(item);
            });
            return dest;
        } else if (type === "Object") {
            dest = {};
            for (let [k, v] of Object.entries(source)) {
                dest[k] = _cp(v);
            }
            return dest;
        } else {
            returnsource; }};return _cp(source);
};
Copy the code

Of course, this does not handle circular references. Processing cyclic reference is also very simple, use a WeakMap to record the value traversed, each copy before finding out the existence of the value in WeakMap, directly return. So the code after processing the loop reference looks like this

const copy = (source) = > {
    const map = new WeakMap(a);const _cp = (source) = > {
        let dest;
        if (map.get(source)) {
            return map.get(source);
        }

        const type = getType(source);
        if (type === "Array") {
            / / array
            dest = [];
            map.set(source, dest);
            source.forEach((item, index) = > {
                dest[index] = _cp(item);
            });
            return dest;
        } else if (type === "Object") {
            // obj
            dest = {};
            map.set(source, dest);
            for (let [k, v] of Object.entries(source)) {
                dest[k] = _cp(v);
            }
            return dest;
        } else {
            returnsource; }};return _cp(source);
};
Copy the code

Non-recursive implementation

In fact, almost any recursive function can be implemented using non-recursive methods. If you have the experience of using javascript to brush Leetcode, a lot of times if our code uses recursion to solve the problem, there is no tail call optimization (many times some problems do not write tail call optimization method, or I may be too poor), it is easy to appear js call stack too deep error situation, At this point we can just go back to non-recursive writing, and it’s very simple

So let’s just take a look at the nature of j s recursion. J s global is that we have a function call stack, and when we call a function a, that function A is pushed, and when we call a again from within a, a new function A is pushed again. After execution, eject the stack one by one. The process is similar for multiple functions. The essence of non-recursive solution is to use the data structure of queue or stack to simulate the execution process of JS call stack

The pseudo-code is as follows, 99 percent of recursion can be implemented non-recursively with the following idea

  • Declare a stack variable to simulate the call stack

    const stack = [];
    Copy the code
  • The initial parameter is pushed

    stack.push({ param1, param2 });
    Copy the code
  • If the stack is not empty, keep fetching the stack element

    while (stack.length) {
        const item = stack.pop();
        / /...
    }
    Copy the code
  • If the next recursion needs to continue, the arguments of the next recursive call are pushed back on the stack

    while(stack.length) {
    	const item = stack.pop()
      // Continue with the next recursion
      // Each recursive execution flow. if (condition) { stack.push({{param1:nextParam1,param2:nextParam2}})
      } else{... }}Copy the code
  • The current recursive termination condition is not pushed

    while(stack.length) {
    	const item = stack.pop()
      // Continue with the next recursion
      // Each recursive execution flow. if (condition) {/ /...
      } else {
        // Recursive termination condition. Do not stack}}Copy the code

After reading this, you might wonder what happens if the result of each recursive call needs to be the return value of the next recursive call. Take the deep copy of our recursive implementation above

dest[index] = _cp(item);
Copy the code

Actually is easy to understand, recursive, when we are at the next lower level recursive back, we also can explain the assignment in recursive scenarios, the next level returns after the execution of our current level variables are also in our implementation can directly, but the recursive case execution of the current iteration, there is no higher level variables. In this case, we need to pass in the next iteration one more variable that points to the lower result of the current iteration. (In recursive scenarios, the return value of the next level of recursion is set in the upper level; In a non-recursive scenario, the return value of the next level is called from the next level, much like we normally pass a callback function.

while(stack.length) {
	const item = stack.pop()
  // Continue with the next recursion
  // Each recursive execution flow.// dest passed by the previous level
  //
  dest[xx] = xxx
  if (condition) {
    / / attention to dest
    stack.push({{param1:nextParam1,param2:nextParam2,dest: {}}}}else{... }}Copy the code

With that in mind, I’m sure you all know the general idea of converting something like a recursive deep copy into a non-recursive implementation, which is to take an object, level by level, and break it down into keys and values. The following is a detailed analysis

  • First, a deep copy receives a value and then returns a copy value, so a reference to the copy value needs to be created initially. At each level of the iteration, we process the subparts of this reference

    const copy = source= > {
        // Simple values are returned directly
        if(! getType(source)) {return source;
        }
        // Create the ontology of the final deep-copy value returned
        let dest = Array.isArray(source) ? [] : {};
        / /...
    };
    Copy the code
  • Perform the mock call stack procedure mentioned above. In the recursive version, we know that the entry to the recursive function is actually the value of the child node that was visited, and the return value is the copy value of the current child node. As discussed earlier, iteration calls need to be set by passing in a reference value created at the previous level. So we iterate over the call, each time with two values: the original value of the node currently accessed (as in the recursive call) and the reference value used to store the copy (created in the previous iteration).

    // Call stack initial state
    const queue = [{ source, dest }];
    while (queue.length) {
        // source Specifies the node currently accessed
        // dest is used to store the copy reference value
        const { dest, source } = queue.shift();
        // Determine the node type to be accessed
        const type = getType(source);
        //
    }
    Copy the code
  • Each iteration process is discussed here in different cases

    • The access nodes are arrays

      if (type === "Array") {
          / /...
          source.forEach((x, index) = > {
              const xType = getType(x);
          });
      }
      Copy the code
    • The entry is the object (1)

              if (xType === "Object") {
                 // Generate an object reference for the next iteration
                          dest[index] = {};
                 // The next iteration is required
                          queue.push({
                              source: x,
                              dest: dest[index]
                          });
                          return;
                      }
      Copy the code
    • The entry is an array (2)

                      if (xType === "Array") {
                        // Generate an array reference for the next iteration
                          dest[index] = [];
                        // The next iteration is required
                          queue.push({
                              source: x,
                              dest: dest[index]
                          });
                          return;
                      }
      Copy the code
    • Array entries are simple values. Set this value directly to dest

        if(! getType(x)) { dest[index] = x;return;
                      }
      Copy the code
  • Access nodes are objects. Similar to array handling

    • Object keys are objects
    • The object key is an array
    • Object keys are simple values

Plus the circular reference processing is very simple, adding the current source to WeakMap at the end of each iteration. It is ok to judge whether the source of push is in WeakMap when dealing with stack.push of object type each time. If it is described as circular reference in WeakMap, set the value directly without pushing

while (stack.lenght) {
    //....
     if (xType === "Object") {
                    if (map.get(x)) {
                        dest[index] = map.get(x);
                        return;
                    }
                    dest[index] = {};
                    queue.push({
                        source: x,
                        dest: dest[index],
                    });
                    map.set(x, dest[index]);
                    return;
                }
    //....
}
Copy the code

Finally, a deep copy of our non-recursive version is complete. Although it took some effort to implement this copy, the code is much more complex than the recursive version, and the performance may be worse, but if you can look at it from the beginning to the end, and implement it yourself, I believe it will greatly improve my understanding of deep copy and the idea of functional recursion. The next time an interviewer asks you to write a deep copy, write a non-recursive version that will wow the interviewer.

The complete code is as follows

const copy2 = (source) = > {
    if(! getType(source)) {/ / simple values
        return source;
    }
    let dest = Array.isArray(source) ? [] : {};
    const queue = [{ source, dest }];

    const map = new WeakMap(a);while (queue.length) {
        const { dest, source } = queue.shift();
        const type = getType(source);
        if (type === "Array") {
            / / array
            source.forEach((x, index) = > {
                const xType = getType(x);
                if(! getType(x)) { dest[index] = x;return;
                }

                if (xType === "Array") {
                    dest[index] = [];
                    queue.push({
                        source: x,
                        dest: dest[index],
                    });
                    return;
                }

                if (xType === "Object") {
                    if (map.get(x)) {
                        dest[index] = map.get(x);
                        return;
                    }
                    dest[index] = {};
                    queue.push({
                        source: x,
                        dest: dest[index],
                    });
                    map.set(x, dest[index]);
                    return; }}); }else {
            / / object
            for (let [k, v] of Object.entries(source)) {
                const vType = getType(v);
                if(! vType) { dest[k] = v;continue;
                }
                if (vType === "Array") {
                    dest[k] = [];
                    queue.push({
                        source: v,
                        dest: dest[k],
                    });
                }
                if (vType === "Object") {
                    if (map.get(v)) {
                        dest[k] = map.get(v);
                        continue;
                    }
                    dest[k] = {};
                    queue.push({
                        source: v,
                        dest: dest[k], }); } map.set(v, dest[k]); }}}return dest;
};

Copy the code

Code word is not easy, if you feel good, ask for a star or like it!