js 点击闭包_【JS进阶】Javascript 闭包与Promise的碰撞

最近遇到一个比较有意思的题目,解决之后深入地思考了一下。

整理整理我在其中的收获,写个文章分享给大家。

一等公民

进入正题之前再聊聊一个话题 —— 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

相关链接:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值