文章目录
- 第二章 函数式地思考问题——第一个案例
- 2.1 提出问题:某件事只做一次 Our problem – doing something only once
- 1. 方案 1:最好别点第二次 Solution 1 – hoping for the best!
- 2. 方案 2:使用全局标识 Solution2 – using a global flag
- 3. 方案 3:移除处理函数 Solution3 – removing the handler
- 4. 方案 4:改变处理函数 Solution 4 – changing the handler
- 5. 方案 5:禁用按钮 Solution 5 – disabling the button
- 6. 方案 6:重新定义处理函数 Solution 6 – redefining the handler
- 7. 方案 7:使用局部标识 Solution 7 – using a local flag
第二章 函数式地思考问题——第一个案例
第一章我们回顾了函数式编程是什么、论述了应用函数式编程的一些优点、并介绍了使用 JavaScript
过程中需要的一些工具。现在让我们抛开理论,从一个简单的问题入手,看看怎样用函数式编程的方式来解决问题。
本章包含如下内容:
- 考察一个简单又常见的电子商务方面的问题
- 采用几个常见的处理方式来解决这个问题,并考察这些方法存在的缺陷
- 从函数式编程的角度,找出一个方法来解决该问题
- 设计一个可应用于其他问题的高阶解决方案
- 研究如何对函数式的解决方案进行单元测试
由于后续章节会深入论述这里列出的一些主题,因此这里不详细展开。本章只展示函数式编程如何对关注的问题提供不同的视角,具体细节留待后续介绍。本章将作为后续章节的前奏,带您考察一个具体的常见问题,并利用函数式的思维方式予以解决。
2.1 提出问题:某件事只做一次 Our problem – doing something only once
一起来考虑这样一个简单而常见的场景:您开发了一个电商网站,网站用户可以添加商品到购物车,最后单击结算按钮,由绑定的信用卡完成支付。此时,用户不能单击结算按钮两次或更多次,以免造成重复支付。
对应的 HTML
部分可能如下:
<button id="billButton" onclick="billTheUser(some, sales, data)">Bill me</button>
相关的 JavaScript
脚本可能如下:
function billTheUser(some, sales, data) {
window.alert("Billing the user...");
// actually bill the user
}
注意
不推荐把事件处理函数像这样直接写到
HTML
中;更恰当的方式应该是在代码层面完成事件绑定。按我说的去做,别照搬我上面的做法。
以上是对研究的问题和网页的最简单的解释,但对于本章演示来说已经足够了。接下来需要考虑:如何避免重复单击该按钮。如何才能避免用户多次点击呢?这个有趣的问题通常有若干种可能的解决方案,先从不怎么样的方案开始吧。
您能想到几种解决方案呢?一起来看看几个可能的方案,并分析他们的解决质量。
1. 方案 1:最好别点第二次 Solution 1 – hoping for the best!
第一个方案听起来更像个笑话:什么也不做,只是告知用户别点第二次,然后希望他们确实能按要求这么做。页面类似酱紫:
【图 2-1 实际页面截图,提醒用户不要单击一次以上】
显然,这是一种逃避问题的方案。笔者见过几个这样的网站,只提醒用户单击多次存在的风险(如图 2-1 所示)而不作任何改进。“重复支付了么?我提醒过了啊……那是他们自己的问题!”
采用该方案可能会看到这样的代码:
<button id="billButton" onclick="billTheUser(some, sales, data)">Bill me</button>
<b>WARNING: PRESS ONLY ONCE, DO NOT PRESS AGAIN!!</b>
说到底,这并不算一个实用的解决方案,接着来看一个更靠谱的方案。
2. 方案 2:使用全局标识 Solution2 – using a global flag
大多数人多半会首先想到用一个全局变量来记录用户是否单击过按钮,例如定义一个标识位 clicked
,初始化为 false
。当用户单击按钮,该值为 false
,就改为 true
并执行结算逻辑;否则不执行任何逻辑。代码如下:
let clicked = false;
// ...
function billTheUser(some, sales, data) {
if (!clicked) {
clicked = true;
window.alert("Billing the user...");
// actually bill the user
}
}
该方案显然是可行的,但也有几个问题必须解决——
- 全局变量的值可能被意外修改。无论是
JavaScript
还是其他语言,全局变量都不是一个好的解决方案; - 用户如果需要再次购买,还须将其重置为
false
,否则无法进行结算; - 由于依赖了外部变量(
clicked
),测试这段代码也会遇到困难;
因此,该方案也不算一个好方案,继续看下一个。
3. 方案 3:移除处理函数 Solution3 – removing the handler
我们可以横向地考虑问题,与其让按钮避免重复点击,不妨完全消除重复点击的可能性。示例如下。billTheUser()
的第一步是移除按钮绑定的单击处理逻辑,就不存在后续调用的可能了:
function billTheUser(some, sales, data) {
document.getElementById("billButton").onclick = null;
window.alert("Billing the user...");
// actually bill the user
}
但该方案也有瑕疵:
- 代码与按钮紧密耦合,无法在其他地方复用;
- 必须记得重置绑定的处理函数,否则用户无法进行二次购买;
- 由于引入了
DOM
元素,代码测试也会更加困难。
其实也可以在调用该函数时,通过传入按钮的 ID
值来避免函数和按钮之间的紧耦合(该思路也可用于后续的一些方案中)。示例代码如下,注意新增的参数:
<button
id="billButton"
onclick="billTheUser('billButton', some, sales, data)"
>
Bill me
</button>;
相应的函数体的内容也要同步更新,通过传入的按钮 ID
访问该按钮:
function billTheUser(buttonId, some, sales, data) {
document.getElementById(buttonId).onclick = null;
window.alert("Billing the user...");
// actually bill the user
}
改造后的方案要好些了,但本质上还是用的全局元素——不是变量,而是 onclick
的值。尽管做了改进,也不能称其为一个好的方案。继续看下一个。
4. 方案 4:改变处理函数 Solution 4 – changing the handler
将上一个方案略加修改,不移除处理函数,而是替换成一个新函数。此时函数被用作一级对象,赋值给了单击事件。提醒用户已经单击过结算按钮的新函数,可以定义成这样:
function alreadyBilled() {
window.alert("Your billing process is running; don't click, please.");
}
这样,函数 billTheUser()
就可以做如下修改,点击按钮时把新函数 alreadyBilled()
赋值给 onclick
属性:
function billTheUser(some, sales, data) {
document.getElementById("billButton").onclick = alreadyBilled;
window.alert("Billing the user...");
// actually bill the user
}
该方案也是可圈可点的。用户再次点击按钮,将收到一条不让重复结算的提示(从用户体验的角度看,该方案更优);但依然存在和前述方案相同的缺陷(代码紧耦合、状态需要重置、不便测试),因此也不能算一个好方案。
5. 方案 5:禁用按钮 Solution 5 – disabling the button
一个类似的思路是不移除处理函数,而直接禁用按钮,不让用户点击。这可以通过设置按钮的 disabled
属性实现:
function billTheUser(some, sales, data) {
document.getElementById("billButton").setAttribute("disabled", "true");
window.alert("Billing the user...");
// actually bill the user
}
虽然也能满足核心需求,但前面提到的三个问题依然存在(代码紧耦合、状态需重置、测试更困难),同样不是我们想要的答案。
6. 方案 6:重新定义处理函数 Solution 6 – redefining the handler
还有一个思路:不动按钮,直接改变事件处理函数本身。关键在于第二行代码,对变量 billTheUser
赋一个新的值,则相当于动态地改变了函数执行逻辑!第一次调用函数,一切正常,但将函数名与一个新函数关联,也使原函数不复存在了:
function billTheUser(some, sales, data) {
billTheUser = function() {};
window.alert("Billing the user...");
// actually bill the user
}
该方案用到了一个特殊的技巧。第 2 行的重新赋值实际上改变了函数的内部业务逻辑。之后 billTheUser
将成为新的函数(一个空函数)。然而这个方案依然很难测试,更糟糕的是原函数没法恢复,点击状态没法重置。
7. 方案 7:使用局部标识 Solution 7 – using a local flag
最后一个常规方案:借助 IIFE
(Immediately Invoked Function Expression,即【立即执行函数表达式】,第 3 章、第 11 章会具体展开)。该方案延续了标识位的思路,但采用了闭包的特性,因此 clicked
变量是局部变量,对其余部分不可见。代码如下:
var billTheUser = (clicked => {
return (some, sales, data) => {
if (!clicked) {
clicked = true;
window.alert("Billing the user...");
// actually bill the user
}
};
})(false);
注意,clicked
变量是从函数调用的 最后一行 获取到初始值的。
该方案与上面的全局变量方案类似,但使用私有的局部变量是一种改进。该方案的唯一缺点,在于必须重写每一个有相同需求的函数才行(该方案在某种程度上还与下一节即将介绍的函数式方案类似)。好吧,其实并不难搞,只是别忘了,不要重复造轮子的建议(Don’t Repeat Yourself,DRY)。
至此,我们尝试了多种方案来解决这个“只运行一次”的问题,但总是不那么完美。接下来从函数式编程的角度思考这个问题,我们将得到更通用的解。
(本节完)