【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次

news2024/10/5 2:03:12

文章目录

    • 2.2 该问题的函数式解 A functional solution to our problem
      • 1. 高阶函数解 A higher-order solution
      • 2. 高阶函数解的手动测试 Testing the solution manually
      • 3. 高阶函数解的自动测试 Testing the solution automatically
      • 4. 更好的解决方案 Producing an even better solution
    • 2.3 小结 Summary

前言
上一篇围绕【如何只让事件逻辑触发一次】给出的 7 种实现方案都不甚理想,这一节来看看函数式的解法究竟有何高明之处。

2.2 该问题的函数式解 A functional solution to our problem

来看更通用的解法。毕竟,让某函数只执行一次的需求并不那么特殊,很可能其他地方也会用到。不妨确立几个原则:

  • 原函数(即只需要执行一次的函数)可以满足既定需求,仅次而已;
  • 不要改动原函数逻辑;
  • 找到一个新函数,让它来调用原函数,且只调用一次;
  • 找到一个通用的解决方案,以便推广到任意数量原函数上。

小贴士

这里的原则一,也就是“单一职责原则”(the single responsibility principle),对应 S.O.L.I.D 五大原则中的 S 原则:每个函数都应该对单一功能负责。更多 S.O.L.I.D. 原则的介绍,详见 Bob 大叔(即 Robert C. Martin,五大原则的提出者)的文章。

能办到吗?是的,我们将用到一种叫做 **高阶函数(higher-order function)**的函数。它能作用于任意函数上,产生一个新的函数来“只执行一次”原函数。本节将引入高阶函数的相关概念(第 6 章会详细阐述)并着手测试找到的函数式解决方案,在此基础上再做一些改进。

1. 高阶函数解 A higher-order solution

如果不修改原函数,需要创建一个高阶函数,不妨称它为 once()。该函数将接收一个函数作为参数,并返回一个只能运行一遍的新函数(前面提到,第 6 章详解高阶函数时还会提到本节内容,详见“重温只执行一次”(Doing things once, revisited)小节)。

专家提示

UnderscoreLodash 已经有类似的函数实现了,调用 _.once() 即可;Ramda 也提供了 R.once() 方法。大部分函数式编程工具库已经包含了类似功能,因此无需再手动实现。

这里的 once() 函数可能一开始看起来有点牵强,但当您习惯用函数式的方式去思考问题后,就会逐渐认同这种用法,并且觉得这样写非常易于理解:

const once = fn => {
    let done = false;
    return (...args) => {
        if (!done) {
            done = true;
            fn(...args);
        }
    };
};

来看一下这个函数的几个细节:

  • 第一行显示 once() 函数接收一个函数(fn)作参数;
  • 利用闭包,我们像上一节中的方案 7 那样,定义了一个私有的内部变量 done。这里最好不要像前面那样叫它 clicked,因为我们不一定需要通过单击按钮来执行该函数,因此选择了一个更通用的变量名。每次调用 once() 函数都会创建一个新的、唯一的 done 变量。该变量的值只有新返回的函数才能访问到;
  • return (...args) => ... 这行表示 once() 函数将返回一个带有若干参数(一个或多个,也可能没有参数)的新函数。注意,这里用到了第 1 章提到过的展开运算符语法。在旧版 JavaScript 中用的是 arguments 对象,详见 参考资料。新版 JavaScript 语法更简单、更凝练;
  • 在运行 fn() 之前将 true 赋给 done,只是为了防止函数抛异常。当然,如果您非要在函数运行结束后才禁止下一次运行,也可以将赋值语句置于 fn() 执行语句的后面;
  • 以上设置完成后,最终我们将调用原函数。注意第 6 行展开运算符的使用,要将 fn() 所需要的参数传进来。

那么,具体该怎么使用呢?我们都不必将新生成的函数存到一个地方,只需像下面这样编写 onclick 方法:

<button id="billButton" onclick="once(billTheUser)(some, sales, data)">
    Bill me
</button>;

这里尤其要注意语法!当用户单击按钮,接收参数 (some, sales, data) 而被调用的目标函数,并不是 billTheUser() 函数自身,而是将 billTheUser() 作为参数、运行函数 once() 而返回的结果。该结果即为只运行一次的目标函数。

小贴士

注意,once() 函数用到了作一级对象的函数,也用到了箭头函数、闭包、展开运算符。之前第 1 章提到我们会用到这些知识点,除了唯一没用到的递归,这里都兑现了。但是正如滚石乐队所唱的那样,你不可能总是得到你想要的You Can’t Always Get What You Want!)。

至此,我们用函数式编程的方式实现了让一个函数只执行一次。该如何进行测试呢?一起来看看吧。

2. 高阶函数解的手动测试 Testing the solution manually

可以做个简单测试。编写一个 squeak() 函数(squeak 即拟声词“吱”),正常情况下运行会发出一声“吱”:

const squeak = a => console.log(a, " squeak!!");

squeak("original"); // "original squeak!!"
squeak("original"); // "original squeak!!"
squeak("original"); // "original squeak!!"

once() 函数应用到 squeak() 函数,则会得到一个只“吱”一声的函数(注意第 1 行):

const squeakOnce = once(squeak);

squeakOnce("only once"); // "only once squeak!!"
squeakOnce("only once"); // no output
squeakOnce("only once"); // no output

CodePen 网站查看运行结果:

图 2.2:在 CodePen 实测高阶函数 once()

【图 2.2:在 CodePen 实测高阶函数 once()】

以上步骤演示了手动测试 once() 函数的过程,但用到的方法并不十分理想。原因及改进意见,将在下一小节给出。

3. 高阶函数解的自动测试 Testing the solution automatically

手动运行测试太原始了,又累又枯燥,过不了多久就不再想去写测试了。让我们改用 Jasmine 这套测试框架来实现自动测试。按照 Jasmine 官网 给的步骤操作即可。笔者用的是单机版,当时版本为 v2.6.1。实测最新单机版(2022-5-13)已升级到 v4.1.1,于三天前更新(GitHub 地址:https://github.com/jasmine/jasmine/releases):

GitHub 上的最新 jasmine 版本

【GitHub 上的最新 jasmine 版本】

下载压缩包、解压,得到如下目录结构:

Jasmine 项目目录结构

【Jasmine 项目目录结构】

其中最新版 SpecRunner.html 如下:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Jasmine Spec Runner v4.1.1</title>

  <link rel="shortcut icon" type="image/png" href="lib/jasmine-4.1.1/jasmine_favicon.png">
  <link rel="stylesheet" href="lib/jasmine-4.1.1/jasmine.css">

  <script src="lib/jasmine-4.1.1/jasmine.js"></script>
  <script src="lib/jasmine-4.1.1/jasmine-html.js"></script>
  <script src="lib/jasmine-4.1.1/boot0.js"></script>
  <!-- optional: include a file here that configures the Jasmine env -->
  <script src="lib/jasmine-4.1.1/boot1.js"></script>

  <!-- include source files here... -->
  <script src="src/Player.js"></script>
  <script src="src/Song.js"></script>

  <!-- include spec files here... -->
  <script src="spec/SpecHelper.js"></script>
  <script src="spec/PlayerSpec.js"></script>

</head>

<body>
</body>
</html>

放到 VSCodeLive Server 下运行,默认示例结果如下:

Jasmine 项目试运行页面截图

【Jasmine 项目试运行页面截图】

接着,按照 SpecRunner.html 中的注释提示信息,创建文件 src/once.js 存放 once() 函数的定义;再创建文件 tests/once.test.js 存放实际测试用例代码。本节示例代码如下:

src/once.js

const once = fn => {
    let done = false;
    return (...args) => {
        if(!done) {
            done = true;
            fn(...args);
        }
    }
}

tests/once.test.js

describe('once', () => {
    beforeEach(() => {
        window.myFn = () => {};
        spyOn(window, 'myFn');
    });

    it("without 'once', a function always runs", () => {
        myFn();
        myFn();
        myFn();
        expect(myFn).toHaveBeenCalledTimes(3);
    });

    it("with 'once', a function runs one time", () => {
        window.onceFn = once(window.myFn);
        spyOn(window, 'onceFn').and.callThrough();
        onceFn();
        onceFn();
        onceFn();
        expect(onceFn).toHaveBeenCalledTimes(3);
        expect(myFn).toHaveBeenCalledTimes(1);
    });
});

然后将默认示例替换为本节示例:(注意第 18 行和第 21 行)

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>Jasmine Spec Runner v4.1.1</title>

  <link rel="shortcut icon" type="image/png" href="lib/jasmine-4.1.1/jasmine_favicon.png">
  <link rel="stylesheet" href="lib/jasmine-4.1.1/jasmine.css">

  <script src="lib/jasmine-4.1.1/jasmine.js"></script>
  <script src="lib/jasmine-4.1.1/jasmine-html.js"></script>
  <script src="lib/jasmine-4.1.1/boot0.js"></script>
  <!-- optional: include a file here that configures the Jasmine env -->
  <script src="lib/jasmine-4.1.1/boot1.js"></script>

  <!-- include source files here... -->
  <script src="src/once.js"></script>

  <!-- include spec files here... -->
  <script src="spec/once.test.js"></script>
</head>

<body>
</body>

</html>

这里需要注意以下几点:

  • 监视一个函数,务必要关联到一个对象(也可以通过 JasminecreateSpy() 方法创建监听器对象);全局函数与 window 对象关联,这里的 window.fn 表明 fn 是一个全局函数;
  • 一个函数被监视后,Jasmine 会拦截函数的调用并记录下该函数已经被调用,并携带哪些参数、总共调用了几次等等。就我们所关心的问题而言,window.fn 可以简单地设为 null,因为它永远不会被执行;
  • 第一组测试用例用于检测函数执行若干次后,可以获取其执行次数。虽然简单,但要是拿不到这个值,代码就真的可能出错了;
  • 第二组测试用例,我们想考察 once() 函数(即 window.onceFn())有且只被调用了一次。然后 Jasmine 监视到 onceFn 函数并放行。后面调用 fn() 函数的情况也会被记录。正如我们预期的那样,本例中 onceFn() 函数虽然执行了三次,但 fn() 函数只执行了一次。

Live Server 再次打开该页面,效果如下:

图 2.3:针对目标函数用 Jasmine 运行自动测试

【图 2.3:针对目标函数用 Jasmine 运行自动测试】

至此,我们已经熟悉了手动及自动化测试函数式解决方案的具体方法,测试部分暂告一段落。本章的最后,让我们来看看有没有更好的函数式的解决方案。

4. 更好的解决方案 Producing an even better solution

之前的解决方案曾提到过一个不错的想法:在首次单击按钮后,每次单击不是静默地忽略用户的单击操作,而是执行某段逻辑。不妨创建一个新的高阶函数,令其接收第二个参数——从第二次单击开始才会执行的一个函数。命名该函数为 onceAndAfter(),具体代码如下:

const onceAndAfter = (f, g) => {
    let done = false;
    return (...args) => {
        if(!done) {
            done = true;
            f(...args);
        } else {
            g(...args);
        }
    };
};

我们在高阶函数上做了进一步探索,onceAndAfter() 函数接收 两个 函数作参数,并产生一个新的、包含了这两个函数的结果函数。

提示

您也可以通过给函数 g 指定一个默认值来增强原函数 onceAndAfter(),例如 const onceAndAfter = (f, g = () => {}),这样即便不传入第二个参数,函数照样能正常运行,因为它默认调用了一个啥都不干的函数(do-nothing function),而不致于报错。

我们可以像之前那样做个临时测试。先创建一个可以“嘎吱作响”的(即 creak 的本意) creak() 函数。然后与 squeak() 函数一道,放入 onceAndAfter() 函数,这样我们就得到了一个发声函数 makeSound()。它可以发出“吱”一次,随后一直发出“嘎吱”声:

const squeak = (x) => console.log(x, "squeak!!");
const creak = (x) => console.log(x, "creak!!");
const makeSound = onceAndAfter(squeak, creak);

makeSound("door"); // "door squeak!!"
makeSound("door"); // "door creak!!"
makeSound("door"); // "door creak!!"
makeSound("door"); // "door creak!!"

为新函数编写测试用例也不难,只是略长。我们要考察哪个函数被调用了,并查看它们调用了几次:

src/onceAndAfter.js

const onceAndAfter = (f, g = () => {}) => {
  let done = false;
  return (...args) => {
    if (!done) {
      done = true;
      f(...args);
    } else {
      g(...args);
    }
  };
};

spce/onceAndAfter.test.js

describe("onceAndAfter", () => {
    it("should call the first function once, and the other after", () => {
        func1 = () => {};
        spyOn(window, "func1");
        func2 = () => {};
        spyOn(window, "func2");
        onceFn = onceAndAfter(func1, func2);

        onceFn();
        expect(func1).toHaveBeenCalledTimes(1);
        expect(func2).toHaveBeenCalledTimes(0);

        onceFn();
        expect(func1).toHaveBeenCalledTimes(1);
        expect(func2).toHaveBeenCalledTimes(1);

        onceFn();
        expect(func1).toHaveBeenCalledTimes(1);
        expect(func2).toHaveBeenCalledTimes(2);

        onceFn();
        expect(func1).toHaveBeenCalledTimes(1);
        expect(func2).toHaveBeenCalledTimes(3);
    });
});

需要注意的是,函数 func1() 自始至终只调用了一次;类似地,考察函数 func2(),调用次数是从 0 开始的(此时执行的是 func1()),随后的每次调用,其调用次数依次递增。

实测效果如下:

最终实测效果截图

【最终实测效果截图】

2.3 小结 Summary

本章从一个现实的场景出发,考察了一个常见的简单问题,并提出并分析了若干种解决方案,最终选择了函数式的解。我们了解了用函数式编程解决实际问题的方法,并找到了一种基于高阶函数相关特性的更通用的解题思路,可以在不对代码动大手术的情况下,方便地应用到类似场景中。此外,我们还学习了如何为代码编写单元测试来完善开发工作。

最后,我们找到了一个从用户体验角度来说效果更好的解决方案,实现了它的逻辑并通过了自动化单元测试。现在您已经开始掌握用函数式编程思维解决问题的方法了。在接下来的第三章《从函数开始——一个核心概念》,我们将更为深入地考察函数的相关特性——这是所有函数式编程的核心。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2189084.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机毕业设计 基于Python的程序员薪资分析系统的设计与实现 Python+Django+Vue 前后端分离 附源码 讲解 文档

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

1000题-操作系统概述

特性微内核&#xff08;Microkernel&#xff09;宏内核&#xff08;Monolithic Kernel&#xff09;设计哲学精简内核&#xff0c;将非核心功能移至用户空间将所有核心功能集成到单一内核空间中功能集成仅包含最基本的操作系统功能&#xff08;如进程间通信、内存管理基础&#…

[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入

信息收集 IP AddressOpening Ports10.10.11.28TCP:22&#xff0c;80 $ nmap -p- 10.10.11.28 --min-rate 1000 -sC -sV PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 e3:54:…

SkyWalking监控SQL参数

前言 SkyWalking可以记录每个请求中执行的所有SQL&#xff0c;但是默认情况下&#xff0c;SkyWalking不记录SQL参数导致使用起来不是很方便&#xff0c;每次都得看日志才能知道具体的参数。不过SkyWalking提供了一个配置参数&#xff0c;开启后&#xff0c;便可记录SQL执行的参…

【AI学习】Mamba学习(一):总体架构

论文&#xff1a;《Mamba: Linear-Time Sequence Modeling with Selective State Spaces》 作者1&#xff1a;Albert Gu&#xff0c;现在是CMU(卡内基梅隆大学)助理教授&#xff0c;曾在DeepMind 工作。多年来一直推动SSM架构发展。 作者2&#xff1a;Tri Dao&#xff0c;现为…

青少年科普教学系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;基础数据管理&#xff0c;作品信息管理&#xff0c;通知公告管理&#xff0c;视频信息管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;视频信息&…

免费神器!PDF 秒变图片在线转换软件大揭秘

现在电子设备使用的频繁&#xff0c;会收到各种不同格式的文件&#xff0c;为了统一或者使用方便转寒格式就成为了我们日常的需求。今天我们来探讨PDF和图片之间的转换需求&#xff0c;探索pdf转图片在线转换免费工具有哪些。 1.福昕PDF转换大师 链接直通&#xff1a;https:/…

RabbitMQ(死信队列)

一、本文抒写背景 前面我也在延迟队列篇章提到过死信队列&#xff0c;也提到过一些应用场景&#xff01; 今天呢&#xff0c;这篇文章&#xff0c;主要就是实战一个业务场景的小Demo流程&#xff0c;哈哈&#xff0c;那就是延迟关闭订单。 二、开始啦&#xff01;letgo! 首…

健康养生行业为何要搭建自己的专属知识付费小程序平台?集师知识付费系统 集师知识付费小程序 集师知识服务系统 集师线上培训系统

在如今快节奏的生活中&#xff0c;健康养生已成为人们日益关注的话题。从饮食调理到运动健身&#xff0c;再到心理调适&#xff0c;健康养生的内涵不断丰富&#xff0c;市场需求也愈发旺盛。面对这一趋势&#xff0c;健康养生行业搭建自己的专属知识付费小程序平台&#xff0c;…

AAA Mysql与redis的主从复制原理

一 &#xff1a;Mysql主从复制 重要的两个日志文件&#xff1a;bin log 和 relay log bin log&#xff1a;二进制日志&#xff08;binnary log&#xff09;以事件形式记录了对MySQL数据库执行更改的所有操作。 relay log&#xff1a;用来保存从节点I/O线程接受的bin log日志…

文件上传之%00截断(00截断)以及pikachu靶场

pikachu的文件上传和upload-lab的文件上传 目录 mime type类型 getimagesize 第12关%00截断&#xff0c; 第13关0x00截断 差不多了&#xff0c;今天先学文件上传白名单&#xff0c;在网上看了资料&#xff0c;差不多看懂了&#xff0c;但是还有几个地方需要实验一下&#…

初识算法 · 双指针(4)

目录 前言&#xff1a; 复写零 题目解析 算法原理 算法编写 四数之和 题目解析 算法原理 算法编写 前言&#xff1a; 本文是双指针算法的最后一文&#xff0c;以复写零和四数之和作为结束&#xff0c;介绍方式同样是题目解析&#xff0c;算法原理&#xff0c;算法编写…

深入浅出Java多线程(六):Java内存模型

引言 大家好&#xff0c;我是你们的老伙计秀才&#xff01;今天带来的是[深入浅出Java多线程]系列的第六篇内容&#xff1a;Java内存模型。大家觉得有用请点赞&#xff0c;喜欢请关注&#xff01;秀才在此谢过大家了&#xff01;&#xff01;&#xff01; 在并发编程中&#xf…

简码短链测试用例设计报告

文章目录 1.前言2.用户模块2.1 登录2.2 注册2.3 修改个人信息2.4 退出登录 3.短链接分组模块3.1 创建短链接分组3.2 修改短链接分组3.3 删除短链接分组 4.短链接管理模块4.1 创建单个短链接4.2 批量创建短链接4.3 修改短链接信息4.4 分页查询短链接4.5 短链接跳转原始链接4.6 删…

CTK框架(十一):使用的常见问题

目录 1.MF文件路径 2.服务必须要接口类 3.插件名命名要求 4.生命周期问题 5.一个接口对多个实现注意 6.中文输出注意 7.同一插件安装注意 8.添加元数据 9.关于升级插件时遇到的问题 10.不同插件定义资源文件注意路径问题 11.安装插件 12.插件依赖 1.MF文件路径 在…

基于Springboot+VUE的二手奢侈品商城的设计与实现

一、摘要 当前&#xff0c;二手奢侈品市场持续蓬勃发展&#xff0c;吸引了越来越多的消费者。然而&#xff0c;现有的二手奢侈品交易平台在用户体验、安全性和功能方面仍存在一些问题&#xff0c;需要进一步改进。本研究旨在设计和实现一种基于Spring Boot 和 Vue 技术框架的二…

题目:最左边的数字

问题 - 1060 (hdu.edu.cn) 解题思路&#xff1a; 数字很大&#xff0c;使用科学计数法。则&#xff0c;我们需要的是a的整数位&#xff0c;最终求出a即可。 取对数&#xff1a;nlgnmlga&#xff0c;移项&#xff1a;lganlgn-m&#xff0c;接下来我们需要求m。 …

04:(寄存器开发)使用外部中断按键控制LED

寄存器开发 1、选择外部引脚配置2、上升沿触发/下降沿触发3、NVIC的配置4、完整代码 关于外部中断的AFIO&#xff0c;NVIC的基础知识请参考《stm32标准库入门教程》 链接: link 1、选择外部引脚配置 如上图所示&#xff1a;外部中断配置寄存器AFIO_EXTICR(1~4)中选择EXTI(0 ~ …

开源模型应用落地-模型微调-模型研制-环境准备(一)

一、前言 在自然语言处理&#xff08;NLP&#xff09;的快速发展中&#xff0c;语料采集作为基础性的步骤显得尤为重要。它不仅为机器学习模型提供了所需的训练数据&#xff0c;还直接影响模型的性能和泛化能力。随着数据驱动技术的不断进步&#xff0c;如何有效并高效地收集、…

链式前向星(最通俗易懂的讲解)

链式前向新&#xff1a;用于存储图的 边集 数组 前言 当我们存储图的时候&#xff0c;往往会使用 邻接矩阵 或是 邻接表。 邻接矩阵 好写&#xff0c;但太浪费空间&#xff0c;节点一多就存不下&#xff1b; 邻接表 效率高&#xff0c;但涉及指 &#xff0c;不好写容易出错…