The general routine referred to here is to change recursive execution to loop execution in a function. Out of curiosity, I want to find a general way to change recursion to non-recursion, and learn the idea. I found a few articles on the Internet, combined with the understanding of the function call stack, I feel that my summary should be more comprehensive, so I recorded it and communicated with you.

The difference between recursive execution and ordinary nested execution

First look at a simple code to calculate the recursive function of factorial:

// n! = n * (n - 1) * (n - 2) * ... * 1
function getFactorial(n) {
  if (n === 1) {
    return 1;
  } else {
    return n * getFactorial(n - 1);
  }
}

To put it simply, the recursive call means that the function calls itself during its execution. In the next call, if the set recursive end condition is not reached, the process will continue; when the recursive condition ends, the functions in the call chain will return one by one. As in the above code, when n === 1, the recursive end condition will be triggered.

Here we think about the difference between the execution of recursive functions and the normal nested execution of functions? :thinking:

In fact, there is no essential difference. The number of layers of recursive calls and ordinary function nesting calls is also limited. The number of layers is determined by the recursive end condition. It is nothing more than recursion calling itself (intuitively, the execution of the code is back. The number of previous lines). Next, what happens when our Kang Kang function is nested.

How does the computer execute functions nested?

If you understand the principle of computer execution of assembly/machine code, you will know that I might next say runtime function call stack . But I intend to use a simple description to guide you to understand or deepen your impression.

First of all, let me ask a question: During a certain execution of a function, what makes this execution different from another execution?

Simply put, this problem can be understood as the execution context . This context has the following contents:

  1. parameter;
  2. Variables defined in the function body;
  3. return value;

Take a chestnut of nested execution of functions. Function a is currently being executed. At this time, there is a statement to execute function b. At this time, it can be simply understood that the computer has done the following things:

  • Save the execution context of a function;
  • Switch to the context of the b function;
  • The code execution comes to the beginning of the b function, and the b function is executed and returns a value;
  • Switch back to the context of the a function and continue to execute the remaining code of the a function;

This process is the runtime function call stack , and the data structure of the stack is used to switch the context.

We mentioned above that recursive calls can be analogous to ordinary nested calls. It is nothing more than this execution context is switched to the next execution context, and note that there is also control of code execution (as mentioned earlier, recursion is back to the previous line) Number execution). Through the above description, we can get some ideas, simulating recursion, to solve two main problems:

  1. Simulate function call stack and switch execution context;
  2. Control the execution order of the statement, from the code behind back to the execution of the previous code;

The switching of execution context can be simulated with the stack. So how to control the execution order of statements? Let's introduce a Continuation technology.

Continuation

Let's look at an example:

function test() {
  console.log(1);
  console.log(2);
  console.log(3);
}

The above output is 123. We are now going to repackage a function with the same content, but change the execution order of these three lines of code. After 2 is output, it will continue to return to 1, and when 2 is output for the second time, it will continue to output 3 and end, which is Output 12123. Of course, you are not required to write redundant console.log repeatedly.

The computer obtains the address of the next execution instruction through the PC (Program Counter) register. In other words, we need to find a solution to simulate the work of PC registers, look at the following code:

function test2() {
  let continuation = 0;
  let count = 0;
  
  while (true) {
    switch (continuation) {
      case 0:
        console.log(1);
        continuation = 1;
        break;
      case 1:
        console.log(2);
        count++;
        continuation = count < 2 ? 0 : 2;
        break;
      case 2:
        console.log(3);
        return;
    }
  }
}

The above method is to use the continuation variable to simulate the operation of the PC register. According to the logic to change the execution path of the program, the simulation code can continue to execute at the number of lines we want. Here is how to rewrite the recursive function by combining Continuation and simulation stack.

Rewrite the recursive function

Let's take a look at the template code of the routine:

function example(...args) {
  const $stack = []; // 模拟栈操作,切换函数上下文。
  let $result; // 记录每一次函数的执行结果。
  
  /**
   * 调用递归函数,压入新的函数上下文,入栈操作。
   * 函数的参数、函数里面声明的变量都要放入上下文中。
   */
  const $call = (...args) => {
    $stack.push({
      continuation: 0,
      ...args
    });
  };
  
  /**
   * 递归函数执行完毕,切换为上一次的函数上下文,出栈操作。
   * 并记录函数的返回值。
   */
  const $return = result => {
    $stack.pop();
    $result = result;
  };
  
  // 第一次调用。
  $call(...args);
  
  while ($stack.length > 0) {
    const current = $stack[$stack.length - 1];
    switch (current.continuation) {
      // 将递归函数的代码拆分,利用 continuation 控制代码执行。
      // ...
    }
  }
  
  // 返回最后的结果。
  return $result;
}

// 执行 example(...) 开始调用“递归”函数。

It can be seen from the above code that the template uses the two points mentioned above to rewrite the recursive function:

  1. Simulate function call stack and switch execution context;
  2. Use Continuation technology to control the execution order of statements;

Then the previous factorial recursive function is rewritten with this template to look like this:

// n! = n * (n - 1) * (n - 2) * ... * 1
function getFactorial(n) {
  if (n === 1) {
    return 1;
  } else {
    return n * getFactorial(n - 1);
  }
}

// 改为非递归。
function getFactorial2(n) {
  const $stack = [];
  let $result;
  
  const $call = n => {
    $stack.push({
      continuation: 0,
      n
    });
  };
  
  const $return = result => {
    $stack.pop();
    $result = result;
  };
  
  $call(n);
  
  while ($stack.length > 0) {
    const current = $stack[$stack.length - 1];
    switch (current.continuation) {
      case 0: {
        const { n } = current;
        if (n === 1) {
          $return(1);
        } else {
          $call(n - 1);
          current.continuation = 1;
        }
        break;
      }

      case 1: {
        const { n } = current;
        $return(n * $result);
        break;
      }
    }
  }
  
  return $result;
}

The key point of understanding here is how to split and rewrite the code. In principle, the place where the recursive function is called must be split. For example, the original code is return n * getFactorial(n - 1); , which is split into two parts:

case 0: {
  const { n } = current;
  if (n === 1) {
    $return(1);
  } else {
    $call(n - 1); // 调用递归函数的地方。
    current.continuation = 1;
  }
  break;
}

case 1: {
  const { n } = current;
  $return(n * $result); // 这里是拿到子递归的返回值并返回这次递归的结果。
  break;
}

Because you want to simulate the process of function calling, you can only execute the following code after there is a return value, so you must split it like this. Also note that because the while loop is used to rewrite the recursion, when it comes to some syntactic sugar or for..in loops, it must be rewritten to the corresponding while loop code.

Let's look at a more complicated example, the object deep copy recursive function:

// JS 对象深拷贝,简单实现。
function deepCopy(obj, map = new Map()) {
  if (typeof obj === 'object') {
    let res = Array.isArray(obj) ? [] : {};
    if (map.has(obj)) {
      return map.get(obj);
    }
    map.set(obj, res);
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        res[key] = deepCopy(obj[key], map);
      }
    }
    return map.get(obj);
  } else {
    return obj;
  }
}

function deepCopy2(obj, map = new Map()) {
  const $stack = [];
  let $result = {};

  const $call = (obj, map) => {
    $stack.push({
      continuation: 0,
      obj,
      map,
      res: null,
      keys: [],
      key: ''
    })
  };

  const $return = obj => {
    $stack.pop();
    $result = obj;
  };

  $call(obj, map);

  while ($stack.length > 0) {
    const current = $stack[$stack.length - 1];

    switch (current.continuation) {
      case 0:
        {
          const { obj, map } = current;
          if (typeof obj === 'object') {
            current.res = Array.isArray(obj) ? [] : {};
            if (map.has(obj)) {
              $return(map.get(obj));
              break;
            }
            current.continuation = 1;
            break;
          } else {
            $return(obj);
            break;
          }
        }

      case 1:
        {
          const { obj, map, res } = current;
          map.set(obj, res);
          for (const key in obj) {
            if (obj.hasOwnProperty(key)) {
              current.keys.push(key);
            }
          }
          current.continuation = 2;
          break;
        }

      case 2:
        {
          if (current.keys.length > 0) {
            current.key = current.keys.shift();
            const { obj, map, key } = current;
            $call(obj[key], map);
            current.continuation = 3;
            break;
          } else {
            $return(map.get(current.obj));
            break;
          }
        }

      case 3:
        {
          current.res[current.key] = $result;
          current.continuation = 2;
          break;
        }
    }
  }

  return $result;
}

In this example, the for..in loop appears, so it must first become a way that the while loop can handle; another point to note is res[key] = deepCopy(obj[key], map); , so the key is also in the context that needs to be saved Variable.

Summarize

In theory, all recursive functions can be rewritten into non-recursive forms using the above template routines. But I think this method does not have much practicality. In essence, it simulates the implementation of the function call stack, and this kind of rewriting will make the code update complicated and difficult to understand. But on the other hand, I think it is valuable to understand and learn this rewriting method, which can deepen the understanding of the function call stack and how the computer runs code control, which is still a bit interesting.

Welcome to star and follow my JS blog: JavaScript


deepfunc
776 声望634 粉丝