5

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

 2 years ago
source link: https://developer.51cto.com/article/715944.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
947c185359868b04c07259b38f6ec97e860c08.jpg

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

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

1、once

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

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

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Document</title>
</head>
<body>
 <div>
   <input type="text" username>
   <input type="password" password>
   <button id="submit">submmit</button>
 </div>
 <script>
   document.getElementById('submit').onclick = function(){
     console.log("sending data to the server")
   }
</script>
</body>
</html>

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

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

6893a33673205d0ac134533229ac571d020747.gif

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

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

document.getElementById('submit').onclick = function()
 document.getElementById('submit').disabled = true
 console.log("sending data to the server")
}
24047395743d334e030269d61bb7a0f36cb3e1.gif

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

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

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Document</title>
</head>
<body>
 <div>
   <input type="text" username>
   <input type="password" password>
   <button id="submit">submmit</button>
 </div>
 <script>
   let hasSubmit = false
   document.getElementById('submit').onclick = function(){
     if(hasSubmit) return;
     console.log("sending data to the server")
     hasSubmit = true
   }
</script>
</body>
</html>

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

23ac3d7221bc3ebdd6b64298f07369680d4faf.gif

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

72b8e5f182e08317fd4204cc5e1e9b504660b2.jpg

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

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

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

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

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

let hasInitialized = false
let init = function(){
 if(hasInitialized) return;
 console.log('init the enviorment')
 hasInitialized = true
}

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

998645c63cf45139c2c362f25e8ab79ea7a684.jpg

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

e9ad9b84694927ccec1777b99def88f2f37ec4.jpg

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

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

34e3ca6773c1a09ee968640546e1af603db2d1.jpg

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

这是一次函数的实现:

function once(func) {
 let hasExecuted = false;
 let result;
 return function () {
   if (hasExecuted) return result;
   hasExecuted = true;
   result = func.apply(this, arguments);
   func = null;
   return result;
 };
}

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

提交一次:

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

初始化一次:

13d0f13835afdd0545c9514c1d024a9ac56397.jpg

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

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

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

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

  • 我们不需要改变原来的功能。
  • 保留业务逻辑和执行逻辑的分隔符,这样代码会更易于维护。
  • 一次函数是一个可重用的函数。

2、cache

让我们来看另一个例子。

如果有这样的一个功能:

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

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

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

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

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

0746b79266f0ce627b57406ce471202331237c.jpg

这是缓存结果的实现:

function cached(fn){
 // Create an object to store the results returned after each function execution.
 const cache = Object.create(null);
 // Returns the wrapped function
 return function cachedFn (str) {
   // If the cache is not hit, the function will be executed
   if ( !cache[str] ) {
       let result = fn(str);
       // Store the result of the function execution in the cache
       cache[str] = result;
   }
   return cache[str]
 }
}

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

a92b1903800a4c6830f107f9ddb1555360846a.jpg

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

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

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返回斐波那契数列的索引值。

怎么写函数?

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

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

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

214fb4a50484917c411883f62c3d1a9306b94e.gif

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

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

3、intercept

让我们继续。

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

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

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

那你会怎么做?

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

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

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

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

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

f59f70d8308228e2002306bc996291142df55b.jpg

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

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

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

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

// index.js
importre request from './request';
const _request = deprecate(request, 'fetch');
export {
 request: _request
}
现在

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

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

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

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

老规矩,先画个图:

57c74530015c05196bf324b48eb001bd610f82.jpg

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

function toJSON(fn) {
 return function() {
   let res = fn.apply(this, arguments)
   try{
     let json = JSON.parse(res)
     return json
   } catch(e){
     return res
   }
 }
}
a98617e8873be29a53d548259bddb8416c7e46.jpg

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

  • derecate功能旨在在执行原始功能之前执行某些操作。
  • toJSON函数旨在执行原始函数后执行某些操作。
17de41c84e3b228494d216eaa4492696d3a651.jpg

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

我们当然可以。

function intercept(fn, {before = null, after = null}) {
 return function () {
   if(before != null) {
     before.apply(this, arguments)
   }
   const result = fn.apply(this, arguments)
   if(after != null){
     after.call(this, result)
   }
   return result
 };
}

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

4、Batch

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

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

function double(num){
 return num * 2
}

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

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

我们可以这样写:

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

确实可以这样写。

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

function double(arg){
 if(Array.isArray(arg)){
   return nums.map(num =>  num * 2)
 }
 return num * 2
}

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

a25f3c962cadcec54d4042d85cabd96f7b9f20.jpg

这是一个实现:

function batch(fn) {
 return function(subject, ...args) {
   if(Array.isArray(subject)) {
     return subject.map((s) => {
       return fn.call(this, s, ...args);
     });
   }
   return fn.call(this, subject, ...args);
 }
}
94f3f8406013d4fd7449348a81119a08352f92.jpg

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

  • 我们想要一个只执行一次的函数,我们可以用  abstract  once。
  • 我们想要一个函数来缓存相应参数的结果,我们可以 abstract  cache 。
  • 我们想要一个在执行前后做某事的函数,我们 可以 abstract  intercept。
  • 我们想要一个通过参数类型改变其执行流程的函数,我们可以 abstract batch。
  • 它们都遵循一个共同的范式:即使用高阶函数来abstract 任何一般过程。

Nested

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

264fb3607753b150eb8537bd2193ece37e3495.jpg

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

let computedEnhance = batch(intercept(cached(computed), {
 before: arg => {
   console.log(`processing ${arg}`)
 },
 after: res => {
   console.log(`returned ${res}`)
 }
}))

Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK