【问题标题】:How to convert sync and async recursive function to iteration in JavaScript如何在 JavaScript 中将同步和异步递归函数转换为迭代
【发布时间】:2018-06-05 13:52:31
【问题描述】:

寻找同步和异步递归函数的特定实现,可以用作将未来递归函数转变为平面迭代的起点。

以下是递归函数的两个示例:同步异步

我正在寻找的是同时使用 无递归堆栈的实现。

例如,它可能会这样工作:

var output = syncStack(myRecursiveFunctionTurnedIterative, [])

或者,如果这不可能,那么只需使用堆栈重新实现下面的两个函数,这应该是一个足够好的开始。例如

var stack = []

function circularReferences(object, references, stack) {
  var output = {}
  if (object.__circularid__) return true
  Object.defineProperty(object, '__circularid__', { value: id++ })
  for (var key in object) {
    var value = object[key]
    if (value && typeof value == 'object') {
      console.log(value)
      stack.push(???)
      circularReferences()
      stack.pop()
      if (is) output[key] = '[Circular]'
    } else {
      output[key] = value
    }
  }
}

提出这个问题的原因是,多年来我一直在尝试学习如何做到这一点,但从未找到一个 (a) 易于记住如何做,并且 (b) 实用的系统。

同步

var references = {}
var object = {
  a: {
    b: {
      c: {
        d: {
          e: 10,
          f: 11,
          g: 12
        }
      }
    }
  }
}
object.a.b.c.d.x = object
object.a.b.c.d.y = object.a.b

var id = 1

var x = circularReferences(object, references)
console.log(x)

function circularReferences(object, references) {
  var output = {}
  if (object.__circularid__) return true
  Object.defineProperty(object, '__circularid__', { value: id++ })
  for (var key in object) {
    var value = object[key]
    if (value && typeof value == 'object') {
      console.log(value)
      var is = circularReferences(value, references)
      if (is) output[key] = '[Circular]'
    } else {
      output[key] = value
    }
  }
}

异步

var items = [
  async1a,
  async1b,
  async1c
  // ...
]

asynca(items, function(){
  console.log('done')
})

function asynca(items, callback) {
  var i = 0

  function next() {
    var item = items[i++]
    if (!item) return callback()

    item(next)
  }
}

function async1a(callback) {
  // Some stuff...
  setTimeout(function(){
    if (true) {
      var items = [
        async2a,
        // ...
      ]

      asynca(items, callback)
    } else {
      callback(null, true)
    }
  }, 200)
}

function async1b(callback) {
  // Some stuff...
  setTimeout(function(){
    if (true) {
      var items = [
        async2a,
        // ...
      ]

      asynca(items, callback)
    } else {
      callback(null, true)
    }
  }, 200)
}

function async1c(callback) {
  // Some stuff...
  setTimeout(function(){
    if (true) {
      var items = [
        async2a,
        // ...
      ]

      asynca(items, callback)
    } else {
      callback(null, true)
    }
  }, 200)
}

function async2a(callback) {
  return callback()
}

例如,这可能开始看起来像:

var items = [
  async1a,
  async1b,
  async1c
  // ...
]

asynca(items, function(){
  console.log('done')
}, [])

function asynca(items, callback, stack) {
  var i = 0

  function next() {
    var item = items[i++]
    if (!item) return callback()
    stack.push(item)
  }
}

但这就是我迷路的地方。不确定如何传递堆栈以及一般应如何设置函数

想知道如何在实践中将它们编写为非递归函数。我见过Way to go from recursion to iteration,但它们都非常理论化。

【问题讨论】:

  • 没有一种解决方案适用于所有情况。或者反过来:如果你构建了一个涵盖所有情况的解决方案,那么这个解决方案对于大多数情况来说都太复杂了。你为什么要问这个问题?你在找什么?
  • 我只是在寻找它的设计模式。我还没有找到一个有意义的。
  • @LancePollard 您无法以真正通用的方式重写它们的原因之一是您遍历了任意深度,其中每个节点都有任意数量的子节点。你可以到达一半,但最终会发生的是你最终会在while 循环内嵌套while 循环(或forfor 内),其中循环数与预期匹配最大深度。您可以通过对深度数字进行硬编码来解决其中的一些问题,但要自动执行此操作,无论如何您都需要递归,然后您不妨在此完成工作。
  • @LancePollard 以及涉及回调的异步问题,这根本行不通。如果您的问题的大小保证了序列化和启动另一个进程/工作者所产生的成本,那么有一些方法可以在不破坏堆栈或不完全阻塞主线程的情况下进行递归,甚至推迟到单独的线程。但是如果没有某种形式的代码生成,你将无法展开到任意深度来执行通用任务,而没有某种形式的递归。
  • 您可能有兴趣了解即将推出的 asyncitertaor 接口:github.com/tc39/proposal-async-iteration

标签: javascript node.js algorithm asynchronous recursion


【解决方案1】:

为了让我们将带有调用另一个函数的函数的过程转换为另一个函数(无论它是否是同一个函数,也称为“递归”,都没有区别),我们需要将它分离到发生的过程中在这样的呼叫之前以及呼叫之后的任何程序。如果out-call之后没有过程并且out-call是针对同一个函数,我们可以将其描述为“尾递归”,它可以使转换为迭代简单得多,只需将调用参数压入堆栈即可(Example)。事实上,将尾递归转换为迭代堆栈过程已经帮助我在不止一个真实实例中克服了浏览器的递归深度限制。

转换为尾递归

为了将递归转换为尾递归,我们必须考虑如何处理从递归调用传递的信息,以及我们是否可以转换此过程以利用递归本身中的参数。由于在您的特定示例中,呼叫结果唯一发生的是局部变量output 的设置,而output 是一个对象,在 JavaScript 中它是通过引用传递的,我们处于一个位置进行这种转变。所以这里有一个简单的重构,可以让我们使用简洁的堆栈(我跳过了堆栈实现的尾递归代码;留给读者作为练习):

var references = {}
var object = {
  a: {
    b: {
      c: {
        d: {
          e: 10,
          f: 11,
          g: 12
        }
      }
    }
  }
}
object.a.b.c.d.x = object
object.a.b.c.d.y = object.a.b

var id = 1

//var x = circularReferences(object, references)
//console.log(x)

//function circularReferences(object, references) {
// => add parameters, 'output' and 'key'
var stack = [[object, references, null, null]];

while (stack.length){
  [_object, _references, _callerOutput, _key] = stack.pop()

  var output = {}
  
  if (_object.__circularid__){
    _callerOutput[_key] = '[Circular]'
    
    // Log our example
    console.log('OUTPUT VALUE: ' + JSON.stringify(_callerOutput))
    
    // Exit
    continue;
  }
  
  Object.defineProperty(_object, '__circularid__', { value: id++ })
  
  for (var key in _object) {
    var value = _object[key]
    
    if (value && typeof value == 'object') {
      console.log(value)
      
      //var is = circularReferences(value, references)
      // if (is) output[key] = '[Circular]'
      stack.push([value, _references, output, key])
        
    } else {
      output[key] = value
    }
  }
}

概括堆栈和排序操作

由于将递归转换为尾递归可能并不总是那么容易和直接,让我们考虑如何使用堆栈以类似于原始递归的方式迭代地对操作进行排序。我们还将稍微概括一下我们的堆栈,这将有助于我们处理您的第二个“异步”示例。让我们不仅存储调用参数,还存储要调用的函数以及参数。比如:

(stack) [
  [function A, parameters for this call of A, additional refs for this call of A],
  [function B, parameters for this call of B, additional refs for this call of B]
]

众所周知,堆栈的操作是“后进先出”,这意味着如果我们有一个函数的操作是在对另一个函数的 out-call 之后进行的,那么这些后续操作将需要被压入堆栈 在调用之前,以便堆栈中的处理顺序类似于:

(stack) [first_call]
pop stack
  => first_call:
       process procedure before out_call
       push procedure after out_call
         => (stack) [procedure after out_call]
       push out_call
         => (stack) [procedure after out_call,
                     out_call]
pop stack
  => out_call
     (maybe followed by a whole other series of stack interactions)
pop stack
  => procedure after out_call (maybe use stored result)

(所有这些都是利用堆栈概念对我们的操作进行排序的技巧。如果您想获得真正的花哨(甚至更复杂),请将每个指令编码为一个函数并模拟一个实际的call stack,它能够暂停主程序中的下一条指令,因为对其他函数的调用被推送到它。)

现在让我们将这个想法应用到您的示例中:

同步示例

我们这里不仅有调用后的过程,而且我们有一个完整的 for 循环来处理这些调用。 (请注意,直接在 sn-p 查看器中查看的控制台日志是不完整的。请观察浏览器的 JS 控制台以获取完整的日志。)

var references = {};
var object = {
  a: {
    b: {
      c: {
        d: {
          e: 10,
          f: 11,
          g: 12
        }
      }
    }
  }
};

object.a.b.c.d.x = object;
object.a.b.c.d.y = object.a.b;

var id = 1;


let iterativeProcess = {
  stack: [],
  currentResult: undefined,
  start: function(){
    // Garbage collector :)
    iterativeProcess.currentResult = undefined

    console.log('Starting stack process')
    
    // Used for debugging, to avoid an infinite loop
    let keep_going = 100;
    
    while (iterativeProcess.stack.length && keep_going--){
        let [func_name, func, params, refs] = iterativeProcess.stack.pop();

        console.log('\npopped: [' + func_name + ', ' + params + ', ' + JSON.stringify(refs) + ']');
        
        params.unshift(refs);
        
        func.apply(func, params);
    }
    
    return 'Stack process done\n\n';
  }
};


let circularReferences = {
  preOutCall: function(refs, _object, _references){
    var output = {};
    
    if (_object.__circularid__){
      console.log('preOutCall: _object has __circularid__ setting currentResult true')
      iterativeProcess.currentResult = true;
      
      // Exit
      return;
    }
    
    Object.defineProperty(_object, '__circularid__', { value: id++ })
    
    // Push post-out-call-procedure to stack
    console.log('Pushing to stack postOutCall ' + Object.keys(_object)[0])
    iterativeProcess.stack.push(['postOutCall', circularReferences.postOutCall, [], output]);
    
    // Call for-loop in reverse
    let keys = Object.keys(_object);

    for (let i=keys.length-1; i >=0; i--)
      circularReferences.subroutineA(output, _object, keys[i], _references);
  },
  
  subroutineA: function(refs, _object, key, _references){
    var value = _object[key];
      
    if (value && typeof value == 'object'){
      console.log('subroutineA: key: ' + key + '; value is an object: ' + value);
      
      console.log('Pushing to stack postSubroutineA ' + key)
      iterativeProcess.stack.push(['postSubroutineA', circularReferences.postSubroutineA, [key], refs]);
      
      // Push out-call to stack
      console.log('Pushing to stack preOutCall-' + key)
      iterativeProcess.stack.push(['preOutCall-' + key, circularReferences.preOutCall, [value, _references], refs]);
  
    } else {
      console.log('subroutineA: key: ' + key + '; value is not an object: ' + value);
      console.log('Pushing to stack subroutineA1 ' + key)
      iterativeProcess.stack.push(['subroutineA1', circularReferences.subroutineA1, [key, value], refs]);
    }
  },
  
  subroutineA1: function(refs, key, value){
    console.log('subroutineA1: setting key ' + key + ' to ' + value);
    
    refs[key] = value;
  },
  
  postSubroutineA: function(refs, key){
    let is = iterativeProcess.currentResult; //circularReferences(value, _references)
        
    if (is){
      refs[key] = '[Circular]';
      
      console.log('postSubroutineA: Object key: ' + key + ' is circular; output: ' + JSON.stringify(refs));
      
    } else {
      console.log('postSubroutineA: key: ' + key + '; currentResult: ' + iterativeProcess.currentResult + '; output: ' + JSON.stringify(refs));
    }
  },
  
  postOutCall: function(){
    // There is no return statement in the original function
    // so we'll set current result to undefined
    iterativeProcess.currentResult = undefined;
  }
};

// Convert the recursive call to iterative

//var x = circularReferences(object, references)
//console.log(x)
console.log('Pushing to stack')
iterativeProcess.stack.push(['preOutCall', circularReferences.preOutCall, [object, references]]);
console.log(iterativeProcess.start());

异步示例

(我冒昧地在asynca 末尾添加了对next() 的调用,我想你忘记了。)

在这里,除了多个交织的函数调用之外,我们还有调用是异步的复杂性,这基本上意味着会有多个堆栈进程。由于在这个特定示例中堆栈进程不会在时间上重叠,我们将只使用一个堆栈,按顺序调用。 (请注意,直接在 sn-p 查看器中查看的控制台日志是不完整的。请观察浏览器的 JS 控制台以获取完整的日志。)

let async = {
  asynca: function(refs, items, callback){
    let i = 0;
    
    function next(refs){
      console.log('next: i: ' + i);
      
      let item = items[i++];
      
      if (!item){
        console.log('Item undefined, pushing to stack: callback');
        iterativeProcess.stack.push(['callback', callback, [], refs]);
        
      } else {
        console.log('Item defined, pushing to stack: item');
        iterativeProcess.stack.push(['item', item, [next], refs]);
      }
    }
      
    console.log('asynca: pushing to stack: next');
    iterativeProcess.stack.push(['next', next, [], refs]);
  },

  async1a: function(refs, callback) {
    // Some stuff...
    setTimeout(function(){
      if (true) {
        var items = [
          async.async2a,
          // ...
        ]
  
        console.log('async1a: pushing to stack: asynca');
        iterativeProcess.stack.push(['asynca', async.asynca, [items, callback], refs]);
        
      } else {
        console.log('async1a: pushing to stack: callback');
        iterativeProcess.stack.push(['callback', callback, [null, true], refs]);
      }
      
      // Since there was a timeout, we have to restart the stack process to simulate
      // another thread
      iterativeProcess.start();
    }, 200)
  },

  async1b: function(refs, callback) {
    // Some stuff...
    setTimeout(function(){
      if (true) {
        var items = [
          async.async2a,
          // ...
        ]
  
        console.log('async1b: pushing to stack: asynca');
        iterativeProcess.stack.push(['asynca', async.asynca, [items, callback], refs]);
  
      } else {
        console.log('async1b: pushing to stack: callback');
        iterativeProcess.stack.push(['callback', callback, [null, true], refs])
      }
      
      // Since there was a timeout, we have to restart the stack process to simulate
      // another thread
      console.log(iterativeProcess.start());
    }, 200)
  },

  async1c: function(refs, callback) {
    // Some stuff...
    setTimeout(function(){
      if (true) {
        var items = [
          async.async2a,
          // ...
        ]
  
        console.log('async1c: pushing to stack: asynca');
        iterativeProcess.stack.push(['asynca', async.asynca, [items, callback], refs]);
        
      } else {
        console.log('async1c: pushing to stack: callback');
        iterativeProcess.stack.push(['callback', callback, [null, true], refs]);
      }
      
      // Since there was a timeout, we have to restart the stack process to simulate
      // another thread
      console.log(iterativeProcess.start());
    }, 200)
  },

  async2a: function(refs, callback) {
    console.log('async2a: pushing to stack: callback');
    iterativeProcess.stack.push(['callback', callback, [], refs]);
  }
}

let iterativeProcess = {
  stack: [],
  currentResult: undefined,
  start: function(){
    // Garbage collector :)
    iterativeProcess.currentResult = undefined

    console.log('Starting stack process')
    
    // Used for debugging, to avoid an infinite loop
    let keep_going = 100;
    
    while (iterativeProcess.stack.length && keep_going--){
        let [func_name, func, params, refs] = iterativeProcess.stack.pop();

        console.log('\npopped: [' + func_name + ', [' + params.map(x => typeof x)  + '], ' + JSON.stringify(refs) + ']');
        
        params.unshift(refs);
        
        func.apply(func, params);
    }
    
    return 'Stack process done\n\n';
  }
};

let _items = [
  async.async1a,
  async.async1b,
  async.async1c
  // ...
];

console.log('Pushing to stack: asynca');
iterativeProcess.stack.push(['asynca', async.asynca, [_items, function(){console.log('\ndone')}]]);
console.log(iterativeProcess.start());

调用堆栈模拟

我不确定我是否有时间来解决这个问题,但这里有一些关于通用模板的想法。将调用其他函数的函数分离为相关的较小函数以启用执行暂停,也许将它们编码为具有操作数组和键的对象以模拟局部变量。

然后编写一个控制器和接口来区分对另一个函数的调用(如果它也有外调用,则进行类似的编码),将该“函数”的对象(或堆栈帧)压入堆栈,记住位置行中的下一条指令。有许多创造性的方法可以用 JavaScript 来完成,例如,使用对象键作为被调用函数的“返回地址”,控制器可以知道。

正如其他人在这里指出的那样,每个调用另一个函数的函数都面临着将其转换为迭代序列的挑战。但是可能有许多功能可以适应这种方案,并允许我们从对执行限制和排序的额外控制中受益。

【讨论】:

【解决方案2】:

我认为这是个坏主意。我认为这样做的好处纯粹是一种智力练习,但你正在强迫一个非常混乱和复杂的答案来解决一个可以通过其他方式解决的简单资源问题。

当 Javascript 中的函数执行时,它会将返回地址、函数参数和局部变量压入堆栈。对于一个深度不定的自然递归问题,通过强制它进入循环,几乎不可能在这些问题上节省很多。先前的响应(我无法输入甚至无法很好地复制粘贴其名称,因为它在 RTL 编码中)讨论了尾递归,如果您可以适应该模式,则可以节省一些不将返回地址推入堆栈的费用,但是无论如何,Javascript 引擎可能会为您处理这些问题。

与其他语言(例如 PHP)相比,一个区别和额外的性能负载是 Javascript 还会为函数的每次调用创建一个闭包。在非常深、非常宽的数据集中,这可能是微不足道的或大量的资源成本,这种类型可能实际上会导致人们可能认为非递归可以解决该问题的问题。但是,有一种方法可以在没有闭包的情况下调用函数,方法是使用 Function 构造函数。我们回避这些,因为 eval-is-evil 并且它们确实需要额外的步骤来编译,但是一旦创建,这可能会减少深度递归中闭包的任何额外开销。减少深度递归调用的开销应该足以弥补实时编译性能的损失。

其他性能拖累可能是创建参数对象和执行上下文 (this)。我们现在还有箭头函数,它们将简单地继承外部作用域的实例。所以实际的递归部分应该是一个箭头函数。如果它需要this,则通过外部Function 的闭包传递它,或者将其作为参数传递。

这是一张总图:

const outerFunction=new Function(
    'arg1,arg2,arg3',
    `
        // Initialize and create items that simply must be transferred by closure
        ...
        const recursiveFunction=(param1,param2,param3)=>{
            ...
            const value=recursiveFunction(newParam1,newParam2,newParam3)
            ...
        }
        return recursiveFunction(firstParam1,firstParam2,firstParam3)
    `
)

一旦进入通用模式,尽可能优化内循环。如果原始对象和数组可以作为引用传递,则不要构建和重新组合对象和数组。不要在递归部分进行任何检查或无关处理。 Javascript 程序员没有太多关于内存泄漏的知识,因为该语言传统上是简短的 sn-ps,当页面刷新时会被擦除,所以要学会发现不再需要的项目不能被垃圾收集的实例,为此如果问题是速度而不是内存,那么可以减少需要垃圾收集的项目。请记住,所有原语都是不可变的,旧值在分配新值时将受到 GC 的影响。考虑在合理的情况下使用类型化数组来防止这种情况发生。

即使这不能解决 OP 的顾虑,我希望这种方法对在 Google 上找到此问题的其他人有所帮助。

【讨论】:

  • 我不熟悉 JavaScript 中的“闭包”究竟是什么。我知道浏览器限制了递归深度,这就是为什么有时迭代堆栈可能是有益的(实际上它对我来说不止一个真实实例)。您如何建议在不将递归转换为迭代的情况下绕过此限制?您描述的关闭问题是否与此有关?
  • 这是我尝试应用您的想法的尝试:ideone.com/nJgDFf 如您所见,代码超出了递归深度限制。您能否分叉我的代码或以其他方式展示我们如何使用您的想法来超越递归深度限制的示例?
  • 不,我不会。您正在构建一个特例示例来支持一般概念,即创建一个通用模式来展平递归在某种程度上不是浪费时间。您的问题是最好通过循环解决的问题,而不是我描述的“无限深度的自然递归问题”。在您的浏览器抱怨之前,您的深度已超过 400 级 - 如果您的实际问题实际上受到此限制的影响,那么请采用特殊情况方法将 THAT 算法放入一个平面循环中。
  • 嘿。我所要求的只是让您提出一个替代方案,以支持您的说法,即使用您没有提供的语言将递归转换为迭代是“一个坏主意”。有无数的递归示例,您很难将其转换为没有堆栈的“循环”,其中一个链接在我的答案顶部附近。除了按照 OP 询问的那样转换递归之外,您对如何处理递归深度限制有什么替代建议?
【解决方案3】:

让我们定义一个简单的函数以及我们的参数。

function syncLoop(iterations, process, exit){
    // Body of the function
}

只是快速谈论参数;

iterations = the number of iterations to carry out
process    = the code/function we're running for every iteration
exit       = an optional callback to carry out once the loop has completed

所以我们有了函数外壳,现在我们需要实例化一个索引和一个布尔值来跟踪我们是否完成了循环。

function syncLoop(iterations, process, exit){
    var index = 0,
        done = false;
    // Body of function
}

现在我们可以跟踪我们的位置,以及我们是否完成了(在循环中这两者都很重要!)。 done 布尔值将成为我们检查是否要在调用时再次实际运行的方法。

是的,这就是它变得稍微复杂的地方。我们将创建一个对象循环,它实际上是我们的循环对象,我们将返回它,以便我们可以从函数外部控制循环。

function syncLoop(iterations, process, exit){
    var index = 0,
        done = false;
    var loop = {
        // Loop structure
    };
    return loop;
}

我一会儿再回来。好的,所以我们有我们的循环。循环中重要的是什么?好吧,我们需要一种访问索引、在循环中继续前进的方法,以及一种终止循环的方法——所以让我们实现这些方法。

function syncLoop(iterations, process, exit){
    var index = 0,
        done = false,
        shouldExit = false;
    var loop = {
        next:function(){
            if(done){
                if(shouldExit && exit){
                    return exit(); // Exit if we're done
                }
            }
            // If we're not finished
            if(index < iterations){
                index++; // Increment our index
                process(loop); // Run our process, pass in the loop
            // Otherwise we're done
            } else {
                done = true; // Make sure we say we're done
                if(exit) exit(); // Call the callback on exit
            }
        },
        iteration:function(){
            return index - 1; // Return the loop number we're on
        },
        break:function(end){
            done = true; // End the loop
            shouldExit = end; // Passing end as true means we still call the exit callback
        }
    };
    return loop;
}

好的,稍微说一下;

loop.next() 是我们的循环控制器。当我们的流程希望完成一个迭代并进入下一个迭代时,它应该调用loop.next()。基本上,loop.next() 所做的只是再次调用我们想要的进程,除非我们完成,在这种情况下它会调用最终回调。

loop.iteration() 函数只返回我们所在的索引。第一次初始化意味着我们将始终是当前迭代之前的一个索引,因此我们返回 index - 1。

loop.break() 只是告诉循环在当前迭代中完成。您可以传递一个可选值来告诉循环正常结束,并在需要时调用exit() 回调。这对于需要自行清理的循环很有用。

是的,我们这里有大部分的身体。所以让我们开始吧,在我们返回循环之前调用loop.next()

function syncLoop(iterations, process, exit){
    var index = 0,
        done = false,
        shouldExit = false;
    var loop = {
        next:function(){
            if(done){
                if(shouldExit && exit){
                    return exit(); // Exit if we're done
                }
            }
            // If we're not finished
            if(index < iterations){
                index++; // Increment our index
                process(loop); // Run our process, pass in the loop
            // Otherwise we're done
            } else {
                done = true; // Make sure we say we're done
                if(exit) exit(); // Call the callback on exit
            }
        },
        iteration:function(){
            return index - 1; // Return the loop number we're on
        },
        break:function(end){
            done = true; // End the loop
            shouldExit = end; // Passing end as true means we still call the exit callback
        }
    };
    loop.next();
    return loop;
}

我们完成了!现在重要的是实现我们的循环并运行它,让我们看一个例子;

syncLoop(5, function(loop){
    setTimeout(function(){
        var i = loop.iteration();
        console.log(i);
        loop.next();
    }, 5000);
}, function(){
    console.log('done');
});

上面的代码简单地打印出我们当前的迭代,每次打印之间有 5 秒的间隔,然后在完成时完成记录。继续在您的浏览器控制台中尝试它。我们还要检查一下 loop.break() 是否按预期工作。

var myLoop = syncLoop(5, function(loop){
    setTimeout(function(){
        var i = loop.iteration();
        console.log(i);
        loop.next();
    }, 5000);
}, function(){
    console.log('done');
});

setTimeout(myLoop.break, 10000);

在这种情况下,我们应该只看到循环结束前打印的前两次迭代。因为我们没有将布尔值传递给 myLoop.break() 它没有完成注销。我们可以通过以下方式改变这一点:

setTimeout(function(){
    myLoop.break(true);
}, 10000);

需要注意的重要一点是,在执行过程中,您不能(干净地)终止循环,它会一直等到当前迭代完成(这实际上很有意义)。它只会将中断排队等待下一次迭代的开始,在 loop.next() 中检查。

【讨论】:

  • 这似乎是获得对任意迭代过程的外部控制的合理方法。这如何回答 OP 的问题,即如何将调用自身的函数转换为适合这种控制的函数?
【解决方案4】:

有一种通用方法可以将递归函数转换为使用显式堆栈:模拟编译器可能处理递归调用的方式。将所有本地状态保存在堆栈上,将参数值更改为将传递给递归调用的值,然后跳转到函数的顶部。然后在函数返回的地方,而不是仅仅返回,检查堆栈。如果非空,则弹出状态并跳转到递归调用将返回的位置。由于 javascript 不允许跳转 (goto),因此需要对代码进行代数以将其转换为循环。

从比原始代码更容易处理的递归代码开始。这只是一个经典的递归 DFS,对于我们已经搜索过的对象(图节点)具有“已访问”标记,对于从最顶层(根)对象到当前对象的路径上的对象具有“当前”标记。如果对于每个对象引用(图边),我们将检查目标是否标记为“当前”,我们将发现所有循环。

当前标记会沿途删除。搜索完图表后,访问过的标记仍然存在。

function get_back_refs(obj, back_refs) {
  if (obj && typeof obj == 'object' && !('__visited__' in obj)) {
    mark(obj, '__visited__')
    mark(obj, '__current__')
    var iter = getKeyIterator(obj)
    while (iter.hasNext()) {
      var key = iter.next()
      if ('__current__' in obj[key]) {
        back_refs.push([obj, obj[key]])
      } else {
        get_back_refs(obj[key], back_refs)
      }
    }
    unmark(obj, '__current__')
  }
}

var object = {
  a: {
    b: {
      c: {
        d: {
          e: 10,
          f: 11,
          g: 12
        }
      }
    }
  }
}
object.a.b.c.d.x = object
object.a.b.c.d.y = object.a.b

var id = 0

function mark(obj, name) {
  Object.defineProperty(obj, name, { value: ++id, configurable: true })
}

function unmark(obj, name) {
  delete obj[name]
}

function getKeyIterator(obj) {
  return {
    obj: obj,
    keys: Object.keys(obj).filter(k => obj[k] && typeof obj[k] == 'object'),
    i: 0,
    hasNext: function() { return this.i < this.keys.length },
    next: function() { return this.keys[this.i++] }
  }
}

var back_refs = []
get_back_refs(object, back_refs)
for (var i = 0; i < back_refs.length; ++i) {
  var pair = back_refs[i]
  console.log(pair[0].__visited__ + ', ' + pair[1].__visited__)
}

请注意,我认为这可以修复您代码中的错误。由于层次结构是一般有向图,因此您希望避免两次搜索对象。跳过这一点很容易导致图形大小的运行时间呈指数增长。然而,图中的共享结构并不一定意味着存在一个循环。该图可以是有向无环图。

在这种情况下,本地状态很好地包含在迭代器中,所以这就是我们在堆栈中需要的全部内容:

function get_back_refs2(obj, back_refs) {
  var stk = []
  var iter = null
 start:
  if (obj && typeof obj == 'object' && !('__visited__' in obj)) {
    mark(obj, '__visited__')
    mark(obj, '__current__')
    iter = getKeyIterator(obj)
    while (iter.hasNext()) {
      var key = iter.next()
      if ('__current__' in obj[key]) {
        back_refs.push([obj, obj[key]])
      } else {
        stk.push(iter) // Save state on stack.
        obj = obj[key] // Update parameter value.
        goto start     // Eliminated recursive call.          
       rtn:            // Where call would have returned.
      }
    }
    unmark(obj, '__current__')
  }
  if (stk.length == 0) return
  iter = stk.pop()  // Restore iterator from stack.
  obj = iter.obj    // Restore parameter value.
  goto rtn
}

现在消除gotos。 This article 描述了一种非常相似的搜索树而不是一般图的转换,所以我不会在这里详述。我们最终得到了这个中间结果:

function get_back_refs2(obj, back_refs) {
  var stk = []
  var iter = null
  for (;;) {
    if (obj && typeof obj == 'object' && !('__visited__' in obj)) {
      mark(obj, '__visited__')
      mark(obj, '__current__')
      iter = getKeyIterator(obj)
      var key = null
      while (iter.hasNext()) {
        key = iter.next()
        if ('__current__' in obj[key]) back_refs.push([obj, obj[key]])
        else break
      }
      if (key) {
        stk.push(iter)
        obj = obj[key]
        continue           
      }
      unmark(obj, '__current__')
    }
    for(;;) {
      if (stk.length == 0) return
      iter = stk.pop()
      obj = iter.obj
      var key = null
      while (iter.hasNext()) {
        key = iter.next()
        if ('__current__' in obj[key]) back_refs.push([obj, obj[key]])
        else break
      }
      if (key) {
        stk.push(iter)
        obj = obj[key]
        break
      }           
      unmark(obj, '__current__')
    }
  }
}

gotos 替换为它们导致执行的代码会导致重复。但是我们可以用一个共享的本地函数把它干掉:

function get_back_refs2(obj, back_refs) {
  var stk = []
  var iter = null
  var descend_to_next_child = function() {
    var key = null
    while (iter.hasNext()) {
      key = iter.next()
      if ('__current__' in obj[key]) back_refs.push([obj, obj[key]])
      else break
    }
    if (key) {
      stk.push(iter)
      obj = obj[key]
      return true           
    }
    unmark(obj, '__current__')
    return false
  }
  for (;;) {
    while (obj && typeof obj == 'object' && !('__visited__' in obj)) {
      mark(obj, '__visited__')
      mark(obj, '__current__')
      iter = getKeyIterator(obj)
      if (!descend_to_next_child()) break
    }
    for(;;) {
      if (stk.length == 0) return
      iter = stk.pop()
      obj = iter.obj
      if (descend_to_next_child()) break
    }
  }
}

除非我犯了代数错误,这当然是可能的,否则这是原始递归版本的直接替换。

虽然该方法不涉及代码代数之外的推理,但现在我们已经完成了,很明显第一个循环会下降到图中,始终是它找到的第一个不是反向引用的子节点,将迭代器推入堆栈。第二个循环弹出堆栈寻找一个迭代器,还有工作要做:至少要搜索一个孩子。当它找到一个时,它将控制权返回给第一个循环。这正是递归版本所做的,以不同的方式表达。

构建一个自动进行这些转换的工具会很有趣。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-25
    • 1970-01-01
    • 1970-01-01
    • 2019-06-19
    • 1970-01-01
    相关资源
    最近更新 更多