什么';调用这种类型的递归是什么?我如何在JavaScript(Node.js)中实现它?

什么';调用这种类型的递归是什么?我如何在JavaScript(Node.js)中实现它?,javascript,node.js,recursion,Javascript,Node.js,Recursion,我知道如何从使用(或)获得的responseText字符串中提取所有href属性(锚定标记)的值,然后创建(唯一)URL的平面对象 我不明白的是如何使用递归(不手动编写每个循环)创建一个嵌套对象(由嵌套对象组成) 这个嵌套的对象具有一定的深度(使用名为深度的参数方便地指定) 例如,假设这是我的代码: function get(url, callback) { // get "responseText" using "requests" // extract the anchor tags

我知道如何从使用(或)获得的
responseText
字符串中提取所有
href
属性(锚定标记)的值,然后创建(唯一)URL的平面对象

我不明白的是如何使用递归(不手动编写每个循环)创建一个嵌套对象(由嵌套对象组成)

这个
嵌套的
对象具有一定的深度(使用名为
深度
的参数方便地指定)

例如,假设这是我的代码:

function get(url, callback) {
  // get "responseText" using "requests"
  // extract the anchor tags href from the "responseText"
  callback({"https://url1.com": {}, "https://url2.com": {});
}

get("https://www.example.com", (urls) => {
  console.log(urls);
});
运行代码时,输出应为:

{ "https://url1.com": {}, "https://url2.com": {} }
我不明白的是如何递归地转到
”https://url1.com“
然后获取此输出:

{ "https://url1.com": { "https://sub-url-1.com": {} }, "https://url2.com": { "https://sub-url-2.com": {} } }
如果深度为5怎么办?我如何递归地遍历每个5级的子URL,然后获取它的子URL


这种类型的递归调用了什么?我如何在JavaScript中实现它?

爬网开始,它接受一个起始url(字符串)和一个起始深度(int),并返回一个承诺的结果。我们的结果是我们预期输出的类型(或“形状”)。在本例中,它是一个以url字符串作为键的对象,值要么是空对象,要么是另一个嵌套结果-

// type url = string
// type result = (url, result) object | empty

// crawl : (string * int) -> result promise
const crawl = (initUrl = '/', initDepth = 0) =>
{ const loop = (urls, depth) =>
    parallel
      ( urls
      , u =>
          depth === 0
            ? [ u, {} ]
            : loop (get (u), depth - 1) 
                .then (r => [ u, r ])
      )
      .then (Object.fromEntries)
  return loop ([ initUrl ], initDepth)
}
垂直样式并不常见,但有助于眼睛识别与制表位垂直规则对齐的代码元素。开放空白允许注释,但随着对样式的熟悉,注释变得不那么必要-

// type url = string
// type result = (url, result) object | empty

// crawl : (string * int) -> result promise
const crawl = (initUrl = '/', initDepth = 0) =>
{ const loop = (urls, depth) =>
    parallel             // parallel requests
      ( urls             // each url
      , u =>             // as u
          depth === 0                   // exit condition
            ? [ u, {} ]                 // base: [ key, value ]
            : loop (get (u), depth - 1) // inductive: smaller problem
                .then (r => [ u, r ])   //   [ key, value ]
      )
      .then (Object.fromEntries)        // convert [ key, value ]
                                        //      to { key: value }

  return loop ([ initUrl ], initDepth)  // init loop
}
这利用了一个通用实用程序
parallel
,它对处理承诺的数组非常有用-

// parallel : (('a array) promise * 'a -> 'b) -> ('b array) promise 
const parallel = async (p, f) =>
  Promise.all ((await p) .map (x => f (x)))
或者如果您不想依赖
async wait
-

// parallel : (('a array) promise * 'a -> 'b) -> ('b array) promise 
const parallel = (p, f) =>
  Promise.all
    ( Promise
        .resolve (p)
        .then (r => r .map (x => f (x)))
    )
给定模拟的
站点地图
和相应的
获取
功能-

// sitemap : (string, string array) object
const sitemap =
  { "/": [ "/a", "/b", "/c" ]
  , "/a": [ "/a/1", "/a/11", "/a/111" ]
  , "/a/1": [ "/a/1/2", "a/1/22" ]
  , "/a/1/2": [ "/a/1/2/3" ]
  , "/a/1/2/3": [ "/a/1/2/3/4" ]
  , "/a/11": [ "/a/11/2", "a/11/22" ]
  , "/a/11/22": [ "/a/11/22/33"]
  , "/b": [ "/b/1" ]
  , "/b/1": [ "/b/1/2" ]
  }

// get : string -> (string array) promise      
const get = async (url = '') =>
  Promise
    .resolve (sitemap[url] || [] )
    .then (delay)

// delay : ('a * int) -> 'a promise
const delay = (x, ms = 250) =>
  new Promise (r => setTimeout (r, ms, x))
我们可以看到
crawl
在不同深度的响应-

crawl ('/') .then (console.log, console.error)
// { '/': {} }

crawl ('/', 1) .then (console.log, console.error)
// { '/': { '/a': {}, '/b': {}, '/c': {} } }

crawl ('/b', 1) .then (console.log, console.error)
// { '/b': { '/b/1': {} } }

crawl ('/b', 2) .then (console.log, console.error)
// {
//   "/b": {
//     "/b/1": {
//       "/b/1/2": {}
//     }
//   }
// }
在这里,我们对根
“/”
进行爬网,深度为
无穷
-

crawl ("/", Infinity) .then (console.log, console.error)
// {
//   "/": {
//     "/a": {
//       "/a/1": {
//         "/a/1/2": {
//           "/a/1/2/3": {
//             "/a/1/2/3/4": {}
//           }
//         },
//         "a/1/22": {}
//       },
//       "/a/11": {
//         "/a/11/2": {},
//         "a/11/22": {}
//       },
//       "/a/111": {}
//     },
//     "/b": {
//       "/b/1": {
//         "/b/1/2": {}
//       }
//     },
//     "/c": {}
//   }
// }
只需将
get
替换为一个实际函数,该函数接受一个输入url并返回一个HREF数组-
crawl
将同样工作

展开下面的代码段,在您自己的浏览器中验证结果-

constparallel=async(p,f)=>
Promise.all((wait p).map(x=>f(x)))
常量爬网=(initUrl=“/”,initDepth=0)=>
{const循环=(URL,深度)=>
平行的
(网址
,u=>
深度===0
?[u,{}]
:循环(获取(u),深度-1)
.然后(r=>[u,r])
)
.then(Object.fromEntries)
返回循环([initUrl],initDepth)
}
//嘲弄
const站点地图=
{”/“:[“/a”、“/b”、“/c”]
,“/a”:[“/a/1”、“/a/11”、“/a/111”]
,“/a/1”:[“/a/1/2”,“a/1/22”]
,“/a/1/2”:[“/a/1/2/3”]
,“/a/1/2/3”:[“/a/1/2/3/4”]
“/a/11”:[“/a/11/2”、“a/11/22”]
,“/a/11/22”:[“/a/11/22/33”]
,“/b”:[“/b/1”]
,“/b/1”:[“/b/1/2”]
}
const get=async(url='')=>
允诺
.resolve(站点地图[url]| |[]))
.然后(延迟)
常数延迟=(x,ms=250)=>
新承诺(r=>setTimeout(r,ms,x))
//演示
爬网(“/”)。然后(console.log、console.error)
// { '/': {} }
爬网(“/”,1)。然后(console.log,console.error)
//{'/':{'/a':{},/b':{},/c':{}
爬网('/b',1)。然后(console.log,console.error)
//{'/b':{'/b/1':{}}
爬网('/b',2)。然后(console.log,console.error)
// {
//“/b”:{
//“/b/1”:{
//“/b/1/2”:{}
//     }
//   }
// }
爬网(“/”,无限)。然后(console.log,console.error)
// {
//   "/": {
//“/a”:{
//“/a/1”:{
//“/a/1/2”:{
//“/a/1/2/3”:{
//“/a/1/2/3/4”:{}
//           }
//         },
//“a/1/22”:{}
//       },
//“/a/11”:{
//“/a/11/2”:{},
//“a/11/22”:{}
//       },
//“/a/111”:{}
//     },
//“/b”:{
//“/b/1”:{
//“/b/1/2”:{}
//       }
//     },
//“/c”:{}
//   }

//}
如果重复链接在对象中创建了循环引用,那就太好了。我可能会写一个这样的答案…@PatrickRoberts这是一个很好的建议<代码>映射
可以用作一种缓存。如果url已经被缓存,我们可以从缓存中获取相应的对象。如果您愿意,可以随意复制这里的任何代码作为起点。我对FP并不像您看起来那么熟悉,所以扩展您的答案是不自然的。不过,我也有同样的想法,使用
Map
作为缓存,所以我肯定会包括在内。为迟到的接受道歉,谢谢你的回答,我感谢你的努力。我没有在你的代码中看到任何递归的尝试?@Bergi尽管这可能会让我看起来很愚蠢,但我真的不知道从哪里开始,我甚至不知道这种递归叫什么:/