热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入理解JavaScript函数式编程技巧与应用(下篇)

本文将继续探讨JavaScript函数式编程的高级技巧及其实际应用。通过一个具体的寻路算法示例,我们将深入分析如何利用函数式编程的思想解决复杂问题。示例中,节点之间的连线代表路径,连线上的数字表示两点间的距离。我们将详细讲解如何通过递归和高阶函数等技术实现高效的寻路算法。

(以下大部分内容翻译自 eloquent Javascript 第7章)

 

来看一个寻路算法的例子:

地图 

如图所示,连线上的数字表示这两点之间的距离,现在要写一个算法能够计算出两点间的最短路径。

别急着往下看,认真地想一下有哪几种可行方案,有哪些可能会碰到的问题。在阅读技术文章时,经常是快速翻阅一遍内容,若有所悟地点点头,然后马上就忘了。如果你真正努力地去解决某个问题,那它就会变成你的问题,它的解决方案也更有意义。

 

首先,用数据来描述这个地图,我们使用一个object:

 roads = { "Point Kiukiu": [ { "to": "Taaoa", "distance": 15 }, { "to": "Hanaiapa", "distance": 19 }, { "to": "Mt Feani", "distance": 15 } ], "Taaoa": [ { "to": "Point Kiukiu", "distance": 15 }, { "to": "Mt Temetiu", "distance": 4 }, { "to": "Atuona", "distance": 3 } ], "Hanaiapa": [ { "to": "Point Kiukiu", "distance": 19 }, { "to": "Airport", "distance": 6 } ], "Mt Feani": [ { "to": "Point Kiukiu", "distance": 15 }, { "to": "Mt Temetiu", "distance": 8 }, { "to": "Airport", "distance": 5 } ], "Mt Temetiu": [ { "to": "Taaoa", "distance": 4 }, { "to": "Mt Feani", "distance": 8 } ], "Atuona": [ { "to": "Taaoa", "distance": 3 }, { "to": "Airport", "distance": 4 }, { "to": "Hanakee pearl lodge", "distance": 1 } ], "Airport": [ { "to": "Mt Feani", "distance": 5 }, { "to": "Atuona", "distance": 4 }, { "to": "Hanaiapa", "distance": 6 }, { "to": "Mt Ootua", "distance": 11 } ], "Hanakee pearl lodge": [ { "to": "Atuona", "distance": 1 }, { "to": "Cemetary", "distance": 6 } ], "Mt Ootua": [ { "to": "Airport", "distance": 11 }, { "to": "Cemetary", "distance": 5 }, { "to": "Hanapaoa", "distance": 3 }, { "to": "Puamua", "distance": 13 } ], "Cemetary": [ { "to": "Hanakee pearl lodge", "distance": 6 }, { "to": "Mt Ootua", "distance": 5 } ], "Hanapaoa": [ { "to": "Mt Ootua", "distance": 3 } ], "Puamua": [ { "to": "Mt Ootua", "distance": 13 }, { "to": "Point Teohotepapapa", "distance": 14 } ], "Point Teohotepapapa": [ { "to": "Puamua", "distance": 14 } ] };

这个roads包含了小岛上所有的路径信息,当我们从某处出发,想要知道有哪些路时,用roads[place]就可以得到,但如果万一打错地名,把place拼错了,就可能意外地返回一个undefined,这可能造成一些很隐蔽的奇怪错误,所以我们用一个函数来获取它

function roadsFrom(place) { var found = roads[place]; if (found == undefined) throw new Error("No place named '" + place + "' found."); else return found;
}
show(roadsFrom("Puamua"));

现在开始尝试一种老老实实的做法,"生成,测试",就像这样:

  1. 生成所有可行的路径。

  2. 在其中找出最短的路径。

对于一个小型的地图来说,生成所有路径的做法是可行的。

 在这之前,我们需要一些新的工具函数,第一个是member,用来检查一个数组中是否包含某个元素。路径是由很多地名组成的数组,当我们到达一个新地方时,我们会调用member来检查这个地方是否已经到过,像这样:

function member(array, value) { var found = false; forEach(array, function(element) { if (element === value) found = true; }); return found;
}
print(member([6, 7, "Bordeaux"], 7)); // true

然而,即使我们要找的值出现在数组的第一项,它也会继续遍历完整个数组,这是没有必要的。当使用for循环时候,我们可以用break来跳出循环,但在forEach中这将不管用,因为循环体是一个函数,break不能跳出函数。解决方法是把forEach改造成能够知道什么时候该停止遍历。

function forEach(array, action) { for (var i = 0; i }

现在,一旦action函数返回false,forEach就会停止遍历。即使member函数使用了forEach,还是显得臃肿且不够通用,它需要在内部维护一个found,最终还要返回它,使得这个函数显得太有针对性和过于麻烦。member实现的是:判断在一组数据中是否有任何满足条件的元素,这个任何可以被抽象出来,成为any函数

function any(test, array) { for (var i = 0; i }
function member(array, value) { return any(partial(op["==="], value), array);
}
print(member(["Fear", "Loathing"], "Denial"));

any遍历整个数组,对每个元素应用test函数,一旦有测试结果为真值,就返回那一项,如果都不满足,返回false。使用any(test, array)就相当于test(array[0]) || test(array[1]) || ...

就像&&有个||为伴, any也有一个与之对应的every:

function every(test, array) { for (var i = 0; i }
show(every(partial(op["!="], 0), [1, 2, - 1]));

我们还需要另一个函数flatten,它接受一个二维数组,将其合并到一个一维数组中:

function flatten(arrays) { var result = []; forEach(arrays, function(array) { forEach(array, function(element) { result.push(element); }); }); return result;
}

当然flattern也可以这样实现:

function flattern2(arrays) { return reduce(function(base, array) { return base.concat(array); }, [], arrays);
}
console.info(flattern2([[1, 3, 5], [2, 4]]));
// 结果为: [1, 3, 5, 2, 4]

但这种做法效率较低,就像反复连接字符串,不如把字符串都加入到数组中然后join起来更高效一样,重复地concat数组会产生很多没必要的临时数组。

在生成路径之前,我们还需要一个函数filter,就像map一样,它接收2个参数,test函数和一个数组array,用于过滤掉array中不满足test的元素。

function filter(test, array) { var result = []; forEach(array, function(element) { if (test(element)) result.push(element); }); return result;
}
show(filter(partial(op[">"], 5), [0, 4, 8, 12]));
// 结果为: [0, 4]

(上面的例子中filter的返回值是否令人不解?记住传给partial的参数,将被用在函数的第一个参数,这里传给partial5,将被作用在op[">"]的第一个参数,所以上面的表达式意思并不是 "所有大于5的值",而是 "5大于的值",即 "比5小的值")

 

现在,整个生成路径的算法看上去应该是这样 -- 从出发点开始,对每一条离开那里的路生成路径,我们使用递归来做:

function possibleRoutes(from, to) { function findRoutes(route) { function notVisited(road) { return ! member(route.places, road.to); } function continueRoute(road) { return findRoutes({ places: route.places.concat([road.to]), length: route.length + road.distance }); } var end = route.places[route.places.length - 1]; if (end == to) return [route]; else return flatten(map(continueRoute, filter(notVisited, roadsFrom(end)))); } return findRoutes({ places: [from], length: 0 });
}
show(possibleRoutes("Point Teohotepapapa", "Point Kiukiu").length); // 11
show(possibleRoutes("Hanapaoa", "Mt Ootua")); // [{ places: ["Hanapaoa", "Mt Ootua"], length: 3 }]

这个函数返回一个路径数组,每一条路径都包含一个所通过节点的数组,和一个总长度。findRoutes递归循环一条路径,返回一个路径的所有延伸。当一条路径的末尾地点是我们的目的地时,它将返回那条路径,因为继续在那条路径上走是没有意义的。如果末尾是其他的地点,我们就继续。包含了flatten/map/filter的那行可能是最难读的,它描述的是:得到所有从当前节点出发的临近节点,从其中过滤掉那些已经访问过的,然后对每一个节点继续进行continueRoute,由于每一个节点都将返回一个路径数组,多个节点map后将返回一个二维数组(第一维对应应节点,第二维对应该节点的路径数组),所以需要用flatten把这二维数组并成一维数组(因为我们并不关心节点,只需要所有的路径数组)。

 这一行做了很多事,这就是抽象带来的好处:不用打满屏幕的代码就能做很复杂的事。

 现在我们有了所有可行的路径,开始尝试找出其中最短的那条。需要一个函数shortestRoute:
 

function shortestRoute(from, to) { var currentShortest = null; forEach(possibleRoutes(from, to), function(route) { if (!currentShortest || currentShortest.length > route.length) currentShortest = route; }); return currentShortest;
}

也许你已经想到了,最短的可以被抽象出来 

 

function minimise(func, array) { var minScore = null; var found = null; forEach(array, function(element) { var score = func(element); if (minScore == null || score }
function getProperty(propName) { return function(object) { return object[propName]; };
}
function shortestRoute(from, to) { return minimise(getProperty("length"), possibleRoutes(from, to));
}

这种最xx的形式还可以抽象出most函数:

function most(arr, comp) { var min = arr[0]; forEach(arr, function(a) { if(comp(min, a)) { min = a; } }); return min;
}
function getProperty(p) { return function(o) { return o[p]; };
}
function minimal(arr, getter) { return most(arr, function(a, b) { return getter(b) } function shortestRoute(from, to) { return minimal(possibleRoutes(from, to), getProperty('length'));
}
console.info(shortestRoute("Point Teohotepapapa", "Point Kiukiu"));

看上去为一个shortestRoute写了这么多行代码,比起第一个版本长了3倍,但这些代码是可重用的。

注意其中的getProperty,这种做法在函数式编程里经常是很有用的。

转:https://www.cnblogs.com/aj3423/archive/2011/02/27/3150515.html



推荐阅读
  • 在 Linux 环境下,多线程编程是实现高效并发处理的重要技术。本文通过具体的实战案例,详细分析了多线程编程的关键技术和常见问题。文章首先介绍了多线程的基本概念和创建方法,然后通过实例代码展示了如何使用 pthreads 库进行线程同步和通信。此外,还探讨了多线程程序中的性能优化技巧和调试方法,为开发者提供了宝贵的实践经验。 ... [详细]
  • 本文深入探讨了JavaScript中`this`关键字的多种使用方法和技巧。首先,分析了`this`作为全局变量时的行为;接着,讨论了其在对象方法调用中的表现;然后,介绍了`this`在构造函数中的作用;最后,详细解释了通过`apply`等方法改变`this`指向的机制。文章旨在帮助开发者更好地理解和应用`this`关键字,提高代码的灵活性和可维护性。 ... [详细]
  • 本文深入探讨了Ajax的工作机制及其在现代Web开发中的应用。Ajax作为一种异步通信技术,改变了传统的客户端与服务器直接交互的模式。通过引入Ajax,客户端与服务器之间的通信变得更加高效和灵活。文章详细分析了Ajax的核心原理,包括XMLHttpRequest对象的使用、数据传输格式(如JSON和XML)以及事件处理机制。此外,还介绍了Ajax在提升用户体验、实现动态页面更新等方面的具体应用,并讨论了其在当前Web开发中的重要性和未来发展趋势。 ... [详细]
  • JavaScript XML操作实用工具类:XmlUtilsJS技巧与应用 ... [详细]
  • 在处理大数相加的问题时,有许多方法可以借鉴。本文介绍了两种不同的函数式编程方法:一种是从网络上找到的经典实现,另一种是作者自行设计的创新方案。通过函数式编程的方式重新实现了这两种方法,其中经典实现简洁明了,而创新方案则在性能和可读性方面有所提升。这些方法不仅适用于大数相加,还可以扩展应用于其他数值计算场景。 ... [详细]
  • 在使用 `useSelector` 选择器时,发现分派操作后状态未能实时更新。这可能是由于 React 组件的渲染机制或 Redux 的状态管理问题导致的。建议检查 `useSelector` 的依赖项和 `dispatch` 的调用时机,确保状态变化能够正确触发组件重新渲染。此外,可以考虑使用 `useEffect` 钩子来监听状态变化,以确保及时更新。 ... [详细]
  • 本文详细介绍了一种利用 ESP8266 01S 模块构建 Web 服务器的成功实践方案。通过具体的代码示例和详细的步骤说明,帮助读者快速掌握该模块的使用方法。在疫情期间,作者重新审视并研究了这一未被充分利用的模块,最终成功实现了 Web 服务器的功能。本文不仅提供了完整的代码实现,还涵盖了调试过程中遇到的常见问题及其解决方法,为初学者提供了宝贵的参考。 ... [详细]
  • 在处理木偶评估函数时,我发现可以顺利传递本机对象(如字符串、列表和数字),但每当尝试将JSHandle或ElementHandle作为参数传递时,函数会拒绝接受这些对象。这可能是由于这些句柄对象的特殊性质导致的,建议在使用时进行适当的转换或封装,以确保函数能够正确处理。 ... [详细]
  • 提升Android开发效率:Clean Code的最佳实践与应用
    在Android开发中,提高代码质量和开发效率是至关重要的。本文介绍了如何通过Clean Code的最佳实践来优化Android应用的开发流程。以SQLite数据库操作为例,详细探讨了如何编写高效、可维护的SQL查询语句,并将其结果封装为Java对象。通过遵循这些最佳实践,开发者可以显著提升代码的可读性和可维护性,从而加快开发速度并减少错误。 ... [详细]
  • 在洛谷 P1344 的坏牛奶追踪问题中,第一问要求计算最小割,而第二问则需要找到割边数量最少的最小割。通过为每条边附加一个单位权值,可以在求解最小割时优先选择边数较少的方案,从而同时解决两个问题。这种策略不仅简化了问题的求解过程,还确保了结果的最优性。 ... [详细]
  • 本文总结了JavaScript的核心知识点和实用技巧,涵盖了变量声明、DOM操作、事件处理等重要方面。例如,通过`event.srcElement`获取触发事件的元素,并使用`alert`显示其HTML结构;利用`innerText`和`innerHTML`属性分别设置和获取文本内容及HTML内容。此外,还介绍了如何在表单中动态生成和操作``元素,以便更好地处理用户输入。这些技巧对于提升前端开发效率和代码质量具有重要意义。 ... [详细]
  • 本文探讨了利用JavaScript实现集合的对称差集算法的方法。该算法旨在处理多个数组作为输入参数,同时保留每个数组中元素的原始顺序。算法不会移除单个数组内的重复元素,但会删除在不同数组之间出现的重复项。通过这种方式,能够有效地计算出多个数组的对称差集。 ... [详细]
  • 2012年9月12日优酷土豆校园招聘笔试题目解析与备考指南
    2012年9月12日,优酷土豆校园招聘笔试题目解析与备考指南。在选择题部分,有一道题目涉及中国人的血型分布情况,具体为A型30%、B型20%、O型40%、AB型10%。若需确保在随机选取的样本中,至少有一人为B型血的概率不低于90%,则需要选取的最少人数是多少?该问题不仅考察了概率统计的基本知识,还要求考生具备一定的逻辑推理能力。 ... [详细]
  • 本文探讨了如何通过检测浏览器类型来动态加载特定的npm包,从而优化前端性能。具体而言,仅在用户使用Edge浏览器时加载相关包,以提升页面加载速度和整体用户体验。此外,文章还介绍了实现这一目标的技术细节和最佳实践,包括使用User-Agent字符串进行浏览器识别、条件加载策略以及性能监控方法。 ... [详细]
  • 能够感知你情绪状态的智能机器人即将问世 | 科技前沿观察
    本周科技前沿报道了多项重要进展,包括美国多所高校在机器人技术和自动驾驶领域的最新研究成果,以及硅谷大型企业在智能硬件和深度学习技术上的突破性进展。特别值得一提的是,一款能够感知用户情绪状态的智能机器人即将问世,为未来的人机交互带来了全新的可能性。 ... [详细]
author-avatar
爱这奋斗
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有