最近遇到一个比较有意思的题目,解决之后深入地思考了一下。
整理整理我在其中的收获,写个文章分享给大家。
一等公民
进入正题之前再聊聊一个话题 —— JavaScript 的一等公民
在 JavaScript 这门语言中,一等公民不仅包含了 变量,对象 等这些名词性的语法,更重要的是 函数 也是一等公民!
因此:函数也可以被当做参数来传递
函数也可以作为函数的返回值
函数也可以被赋值到某个变量
函数也动态地先定义(赋值到某个变量)再执行
正是由于前两点,才引申出了 高阶函数。
高阶函数
“什么是高阶函数?”
面试的时候我特别喜欢问这个问题,因为高阶函数是个特别灵活且实用的模式,包含了JavaScript的重要特性(稍后揭晓)。
看看 Wikipedia 上的对 高阶函数 的定义In mathematics and computer science, a higher-order function is a function that does at least one of the following:
1. takes one or more functions as arguments (i.e. procedural parameters),
2. returns a function as its result.
即当一个函数包含至少一个以下特点时它就是个高阶函数:接收函数作为参数
以函数为返回值
举点例子:任何接收回调函数的函数都是高阶函数,如 addEventListioner, setTimeout 等
数组中的很多操作函数也是高阶函数,如 map,filter,some 等
对传入的函数装饰、增强再返回一个新的函数 的函数也是高阶函数,比如前几期文章中介绍的 immer 的 produce 的柯里用法FreewheelLee:immer —— 提高React开发效率的神器zhuanlan.zhihu.com
闭包
首先看看闭包在JavaScript MDN 官方文档的定义函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。
我有时候会问面试者 —— “你认为闭包的核心意义是什么?”
我期待得到的答案是 —— “保存状态”
什么意思呢?
在实际应用中,闭包在很多场景下都是用来保存外部函数的某些状态,在外部函数执行结束(并返回内部函数)后不至于丢失目标状态,这中间的操作类似于打了一个快照(snapshot),内部函数仍然能持续访问到这个快照 —— 并且还能更新这个快照。
而返回函数的 高阶函数经常用到 闭包 特性来保存某些状态。
Promise
Promise相信大家都了解,是 JavaScript 在 ES6 中用来处理异步操作的新特性 。
比如原生的Http client API fetch 就是返回一个 Promise 来代表一个异步的网络请求
终于进入主题
设计一个反应力游戏:
玩家点击页面上的一个按钮,就会执行一个异步操作,这个异步操作会在 随机的时间段后完成,并产生一次 鸣叫 —— “嘀!”
当玩家听到这个鸣叫声之后就要立即再次点击按钮,如此反复直到完成10次操作,最后统计玩家每次听到鸣叫和点击按钮的时间延迟,时间短的玩家胜出。
此外,假如 还没听到鸣叫声就再次点击按钮 就直接判输淘汰。
除去统计反应时间和实现鸣叫,你会如何实现这个按钮的点击回调函数呢?尝试使用前文提到的高阶函数,Promise 和 闭包。
将场景简单化就会发现其实这个按钮的点击回调函数的核心逻辑是:如果 没有异步操作正在进行 就 触发异步操作
如果 有 异步操作正在进行 就 抛错 或 拒绝操作
显然,这边需要一个保存状态 —— 是否正在执行异步操作。保存状态的方法有很多种:React组件的state,Redux,LocalStorage / SessionStorage,闭包 等等。
接下来我们尝试使用闭包来保存这个状态。
先写个闭包函数整体框架:
const exactlyOnceEachTime = function () {
let processing = false;
return function () { // 这个内部函数能访问到外部函数的 processing 变量
if (processing) {
// 拒绝执行操作
}
processing = true;
// 执行异步操作,异步操作结束后重置 processing 状态
}
}
我们选择Promise来表示异步操作
const exactlyOnceEachTime = function () {
let processing = false;
return function (params) {
if (processing) {
return Promise.reject("The operation is in process!");
}
processing = true;
return new Promise((resolve, reject) => {
// 省略异步操作的具体内容,只是简单放一个计时器
setTimeout(() => {
processing = false; // 重置processing状态
resolve()
}, 1000)
})
}
}
一个最简单的版本看起来就完成了。
但是现实情境中,具体的异步操作都会被封装起来,暴露一个函数以Promise作返回值 —— 比如 Fetch API 或者 axios 的 API
所以让我们进一步优化 exactlyOnceEachTime 让它能接受这样的参数
// 接收一个返回Promise的函数做参数
const exactlyOnceEachTime = function (promiseFunc) {
let processing = false;
return function () {
if (processing) {
return Promise.reject("The operation is in process!");
}
processing = true;
return promiseFunc(); // 这一步还有待改进
}
}
上面的代码缺少了重置 processing 状态的逻辑,会导致即使异步操作执行结束了,也无法执行下一个异步操作。
而 promiseFunc 又是外界传入的参数,我们并不能强行加入重置processing 状态的代码。
怎么办呢?
天空飘来几个字 —— “朋友,你听说过 装饰者模式 吗?”
知道传统装饰者模式的读者可能会想:“什么?装饰者模式不是用于对象的吗?也能用于Promise吗?”
当然!因为思想是共通的。
装饰者模式的核心在于 不改变原有函数/对象/Promise的行为,而增加一层装饰层 —— 增强功能或者引入额外逻辑 —— 且对调用者无感(装饰者本身的类型跟被修饰的对象是一样的)。
如果能理解这种模式的常见实现,各种变式代码也可以很容易写出来。
const exactlyOnceEachTime = function (promiseFunc) {
let processing = false;
return function (params) {
if (processing) {
return Promise.reject("The operation is in process!");
}
processing = true;
const realPromise = promiseFunc(params); // 真正的异步操作Promise
return new Promise((resolve, reject) => { // 装饰者 —— 也是个 Promise,调用者因此无感知
realPromise.then((data) => {
resolve(data);
processing = false; //重置processing状态 (属于装饰者引入的额外逻辑)
}).catch((error) => {
reject(error);
processing = false; //重置processing状态(属于装饰者引入的额外逻辑)
});
})
}
}
最后,写一点测试代码验证一下这个函数的功能
// 返回一个简单的Promise,1s后执行完毕
const giveMeAPromise = function (data) {
return new Promise((resolve) => {
setTimeout(() => resolve(data), 1000)
})
}
console.log("start testing");
const request = exactlyOnceEachTime(giveMeAPromise);
request(1).then(data => {
console.log("process 1 done —— result " + data);
}).catch(e => {
console.log("process 1 rejected. Error: " + e);
}) // 输出结果: process 1 done —— result 1
request(2).then(data => {
console.log("process 2 done —— result " + data);
}).catch(e => {
console.log("process 2 rejected. Error: " + e);
}) // 输出结果:process 2 rejected. Error: The operation is in process!
setTimeout(() => {
request(3).then(data => {
console.log("process 3 done —— result " + data);
}).catch(e => {
console.log("process 3 rejected. Error: " + e);
})
}, 2000) // 输出结果: process 3 done —— result 3
setTimeout(() => {
request(4).then(data => {
console.log("process 4 done —— result " + data);
}).catch(e => {
console.log("process 4 rejected. Error: " + e);
})
}, 2100) // 输出结果: process 4 rejected. Error: The operation is in process!
今天的分享就到这里,关于JavaScript中闭包,Promise 甚至是设计模式,你有什么想法呢?欢迎留言分享。
另外,想要了解更多设计模式在 JavaScript 中的运用,欢迎阅读我的另一篇文章FreewheelLee:什么?JavaScript不用class也能实现设计模式!zhuanlan.zhihu.com
相关链接: