当前位置:   article > 正文

如何像高级 JavaScript 开发人员一样为一般流程编写高阶函数

subject 只执行一次

579dc27e3e9834ea980715b7680c326d.jpeg

英文 | https://betterprogramming.pub/writing-high-order-functions-for-general-process-like-a-senior-javascript-developer-4d025baa3dc6

翻译 | 杨小爱

我们知道函数是解决问题的基本单位。但在现实世界中,一些原有的功能并不能完全解决问题。这时候,我们需要扩展我们现有的代码。

一些编码人员可能会直接更改原始功能以达到某种目的。嗯,这是初级开发人员常用的方法,也是一种直观的方法。

但在很多情况下,它并不是最好的解决方案,并且有一些缺点。在今天的内容中,我将通过示例为您介绍一些通用的解决方案。

好吧,我们开始吧。

1、once

很多时候,我们想要一个只执行一次的函数。

比如,我们开发网页的时候,总会有一些提交表单的按钮。当用户点击按钮时,会触发它的 onclick 事件。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <div>
  11. <input type="text" username>
  12. <input type="password" password>
  13. <button id="submit">submmit</button>
  14. </div>
  15. <script>
  16. document.getElementById('submit').onclick = function(){
  17. console.log("sending data to the server")
  18. }
  19. </script>
  20. </body>
  21. </html>

为了简化演示问题,该示例仅记录一条消息,而不是向服务器发送数据。

但这里有一个问题:由于网络延迟,我们无法立即为用户显示结果。然后用户可能继续点击该按钮并多次向服务器提交表单。

2c673f3e99222c1e37431b823195ea27.gif

所以,我们需要解决这个问题,你的解决方案是什么?

一个常见的解决方案是在用户第一次单击按钮后禁用该按钮。

  1. document.getElementById('submit').onclick = function()
  2. document.getElementById('submit').disabled = true
  3. console.log("sending data to the server")
  4. }

6e475db76f1b6ca98974c6660e2a5805.gif

嗯,这个解决方案没有问题。

另外,我们有一个不同的解决方案:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <div>
  11. <input type="text" username>
  12. <input type="password" password>
  13. <button id="submit">submmit</button>
  14. </div>
  15. <script>
  16. let hasSubmit = false
  17. document.getElementById('submit').onclick = function(){
  18. if(hasSubmit) return;
  19. console.log("sending data to the server")
  20. hasSubmit = true
  21. }
  22. </script>
  23. </body>
  24. </html>

在这个解决方案中,我们使用一个标志来记录该函数之前是否已执行过。

b15e7b43b12e0d1904694405c175ca6f.gif

如果我们使用图表来表示程序,它可能是这样的:

c14bf1d8cdefce3c7fa0b9907775b335.png

但是,我们能否为所有此类问题找到一个通用的解决方案?

让我们继续一个类似的例子。很多时候,我们的程序中有一个init函数。

  1. let init = function(){
  2. console.log('init the enviorment')
  3. }

我们可以使用这个函数来设置变量、读取配置等。这个函数应该只执行一次。为了确保它只执行一次并避免意外,我们可以对函数进行一些更改:

  1. let hasInitialized = false
  2. let init = function(){
  3. if(hasInitialized) return;
  4. console.log('init the enviorment')
  5. hasInitialized = true
  6. }

好的,init函数只会初始化环境一次。

1210f655c1b181dd3d4893fa12447c3e.png

我们还可以将程序绘制成图表。

35a41648ccac25e8d408607eb2f55575.png

你发现表单提交和初始化函数有一些共同点吗?是的,他们的程序非常相似!

如果我们做高级抽象,流程应该是这样的:

158206ca1a1f9ef2b79e0a8449870e92.png

如果该函数之前已被调用是一般程序。我们可以编写一个高阶函数来密封这个过程。

这是一次函数的实现:

  1. function once(func) {
  2. let hasExecuted = false;
  3. let result;
  4. return function () {
  5. if (hasExecuted) return result;
  6. hasExecuted = true;
  7. result = func.apply(this, arguments);
  8. func = null;
  9. return result;
  10. };
  11. }

现在,使用 once 函数,我们可以轻松地归档执行一次函数的目的。

提交一次:

  1. document.getElementById('submit').onclick = once(function()
  2. console.log("sending data to the server")
  3. })

46cb15a58e190a3983231b9044443b8b.gif

初始化一次:

b9e3b5632672b0035c3034656ee33404.png

好的,我们使用 once 函数来解决我们的需求。

使用 once 函数的核心思想是什么?

正如我在标题中提到的:我们将一般过程抽象为高阶函数。程序——只执行一次函数——是一个通用过程。它会被多次使用。如果我们不做抽象,我们就必须在不同的函数中为相同的逻辑重复编写代码。

如果我们使用 once 函数,有很多好处:

  • 我们不需要改变原来的功能。

  • 保留业务逻辑和执行逻辑的分隔符,这样代码会更易于维护。

  • 一次函数是一个可重用的函数。

2、cache

让我们来看另一个例子。

如果有这样的一个功能:

  1. function compute(str) {
  2. // Suppose the calculation in the funtion is very time consuming
  3. console.log('2000ms have passed')
  4. return str.toUpperCase()
  5. }

(其实这个案例我是从 Vue 源码中学到的。)

我们要缓存函数操作的结果。稍后调用时,如果参数相同,则不再执行该函数,而是直接返回缓存中的结果。我们能做什么?

这里有一个建议:当你需要增强一个函数时,不要试图直接修改它,考虑先写一个通用的高阶函数来包装它。

缓存函数结果的一般过程是什么?这是一个流程:

26c173c5dca7dbe4936c221602a8d30f.png

这是缓存结果的实现:

  1. function cached(fn){
  2. // Create an object to store the results returned after each function execution.
  3. const cache = Object.create(null);
  4. // Returns the wrapped function
  5. return function cachedFn (str) {
  6. // If the cache is not hit, the function will be executed
  7. if ( !cache[str] ) {
  8. let result = fn(str);
  9. // Store the result of the function execution in the cache
  10. cache[str] = result;
  11. }
  12. return cache[str]
  13. }
  14. }

现在我们可以使用这个缓存函数来增强 cumpute 函数:

64597e7190aab036aa2270e62b325d51.png

我们做这个抽象并不是为了炫耀技巧,其实这样的缓存功能用途广泛。

我们知道,有一个著名的序列叫做斐波那契数列。

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...

快速浏览后,您可以很容易地注意到序列的模式是每个值都是前 2 个值的总和,这意味着对于 N=5 → 2+3 或在数学中:

F(n) = F(n-1) + F(n-2)

现在我们要写一个函数:

给定一个数字N返回斐波那契数列的索引值。

怎么写函数?

最简单的解决方案是递归解决方案:

  1. function fibonacci(num) {
  2. if (num <= 1) return 1;
  3. return fibonacci(num - 1) + fibonacci(num - 2);
  4. }

但是这个实现很耗时,如果 num 大于 35,您将等待一段时间才能得到结果。

64703ed12e5f192d98f57ccfabcd396d.gif

但是如果我们使用缓存函数来重构实现,我们会得到一个高性能的函数。

  1. let cachedFibonacci = cached(function(num){
  2. if(num <= 1) return 1;
  3. return cachedFibonacci(num - 1) + cachedFibonacci(num - 2)
  4. })

126282985bad8172cd591be40223b575.gif

3、intercept

让我们继续。

假设您是一个库的维护者,并且您将在未来弃用一个名为 request 的旧 API。

  1. function request(){
  2. console.log('request to server')
  3. }

在当前版本中,您希望通过记录消息来警告用户 API 将被弃用。

那你会怎么做?

最糟糕的方法是在函数中添加一个 console.warn 语句:

  1. function request(){
  2. console.warn(`The request will be deprecated in the future`)
  3. console.log('request to server')
  4. }

为什么这是最糟糕的解决方案?

您必须找到所有已弃用的 API 并对其进行修改。这是一个非常繁琐的过程,而且很容易导致错误。如果没有必要,不要更改现有功能。

如果我们用图来表示程序,那就是:

2c74fa4111e58b00c82050cc109282d6.png

正如我们在前面内容中所做的那样,我们可以为该过程编写一个高阶函数。

  1. function deprecate(fn, newApi) {
  2. return function() {
  3. console.log( `The ${fn.name} will be deprecated. Please use the ${newApi} instead.`);
  4. return fn.apply(this, arguments);
  5. }
  6. }

然后我们可以对我们的项目做一些改变:

  1. // index.js
  2. importre request from './request';
  3. const _request = deprecate(request, 'fetch');
  4. export {
  5. request: _request
  6. }

现在,如果您的库的用户调用请求函数,他们将收到一条消息。

好的,让我们继续一个类似的例子。

我们有一个 fetch 函数来向服务器发送请求。它将返回 HTML 文本或 JSON 格式的文本。

  1. var fetch = function(url){
  2. let responseContent = null
  3. console.log(`fetching ${url}`)
  4. if(Math.random() < 0.5){
  5. return '<html><body>hello world</body></html>'
  6. } else {
  7. return '{"name": "bytefish"}'
  8. }
  9. }

我们现在要做的是,如果我们发现响应结果是 JSON 格式的字符串,我们将其转换为 JSON 对象。如果是其他格式的字符串,则不进行处理。我应该怎么办?

老规矩,先画个图:

b10aa4d48cf18c53214ba843b0076d0b.png

具体原理已经解释过很多次了,这里我直接给出一个高阶函数:

  1. function toJSON(fn) {
  2. return function() {
  3. let res = fn.apply(this, arguments)
  4. try{
  5. let json = JSON.parse(res)
  6. return json
  7. } catch(e){
  8. return res
  9. }
  10. }
  11. }

用法:

4663d4a5430fdb75d92f94cc49b61bfe.png

这两个例子有点简单。但附近还有一个更重要的想法。

  • derecate功能旨在在执行原始功能之前执行某些操作。

  • toJSON函数旨在执行原始函数后执行某些操作。

fcb3a17fef587bc56e64d1ee11c37f76.png

我们能把这个过程抽象成一个新的高阶函数吗?

我们当然可以。

  1. function intercept(fn, {before = null, after = null}) {
  2. return function () {
  3. if(before != null) {
  4. before.apply(this, arguments)
  5. }
  6. const result = fn.apply(this, arguments)
  7. if(after != null){
  8. after.call(this, result)
  9. }
  10. return result
  11. };
  12. }

如果你之前用过 Axios 这个著名的 HTTP 请求库,你就会知道 Axios 有一个拦截器 API 供用户拦截请求和响应。

4、Batch

好的,这是我们的最后一个例子。

这是一个将输入加倍的函数。

  1. function double(num){
  2. return num * 2
  3. }

嗯,很简单的功能,只是为了演示。

如果我们想让这个函数接受一个数组作为参数,那么将数组中所有元素的值加倍,然后返回一个新数组。 你怎么写代码?

我们可以这样写:

  1. function double(nums){
  2. return nums.map(num => num * 2)
  3. }

确实可以这样写。

但遗憾的是,JavaScript 没有函数重载,后者的函数会覆盖前者。 为了让我们的double函数同时处理两种参数类型,我们必须在函数体中做出判断:

  1. function double(arg){
  2. if(Array.isArray(arg)){
  3. return nums.map(num => num * 2)
  4. }
  5. return num * 2
  6. }

我们想要的是为所有这些问题创建一个通用的解决方案:一个高阶函数,可以标记一个函数来处理单个参数或类似数组的参数。

ae182b99546ff325a74ba0b7c88ed698.png

这是一个实现:

  1. function batch(fn) {
  2. return function(subject, ...args) {
  3. if(Array.isArray(subject)) {
  4. return subject.map((s) => {
  5. return fn.call(this, s, ...args);
  6. });
  7. }
  8. return fn.call(this, subject, ...args);
  9. }
  10. }

1c03d899435b218972cb1c3e8898cab3.png

总结

我想,我举的例子已经够多了。 无论是once,cache,intercept还是batch,它们都对某个进程进行了一些抽象。

  • 我们想要一个只执行一次的函数,我们可以用  abstract  once。

  • 我们想要一个函数来缓存相应参数的结果,我们可以 abstract  cache 。

  • 我们想要一个在执行前后做某事的函数,我们 可以 abstract  intercept。

  • 我们想要一个通过参数类型改变其执行流程的函数,我们可以 abstract batch。

  • 它们都遵循一个共同的范式:即使用高阶函数来abstract 任何一般过程。

Nested

恩,我想提的最后一件事:如果有必要,我们可以嵌套这些高阶函数。

87477843d27c169d983d238fd3248396.png

假设我们不仅要缓存计算函数的结果,还要在执行它之前记录它的参数,并在执行它之后记录它的结果。 然后,我们还想让它能够处理多重参数。 我们可以这样写:

  1. let computedEnhance = batch(intercept(cached(computed), {
  2. before: arg => {
  3. console.log(`processing ${arg}`)
  4. },
  5. after: res => {
  6. console.log(`returned ${res}`)
  7. }
  8. }))

最后,希望我的这篇文章内容对你有帮助,如果你觉得有用的话,请记得点赞我,关注我,并将它分享给你身边的朋友,感谢阅读,祝编程愉快!

学习更多技能

请点击下方公众号

a10572755d09774318eeca8dc83e47a9.gif

4ddbcf4cf6087d316d5753c2b576e42e.jpeg

1fc28fd09b1205afe7d3f700b8650117.png

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/798944
推荐阅读
相关标签
  

闽ICP备14008679号