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

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

一等公民

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

相关链接:

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

  1. js室内地图开发_室内地图JavaScript SDK地图控制 - 蜂鸟云

    地图控制 Fengmap地图加载完成后,可通过地图方法和地图进行交互. 单/多楼层显示 地图加载完成后可配置楼层显示数量,当地图为多层时,地图数据的楼层ID从groupID =1开始依次向上加1,默认 ...

  2. js逻辑训练题_几道javascript练习题

    走在前端的大道上 问题1: 作用域(Scope) 考虑以下代码: (function() { var a = b = 5; })(); console.log(b); 控制台(console)会打印出 ...

  3. js室内地图开发_室内地图 JavaScript API

    室内地图JavaScript API文档 V1.2 主要功能类: Map API各种类中的核心部分,用来在页面中创建地图并操纵地图. //示例 初始化地图 var map = new Indoor.M ...

  4. html js点击字图片下拉,JavaScript实现文字与图片拖拽效果的方法

    本文实例讲述了JavaScript实现文字与图片拖拽效果的方法.分享给大家供大家参考.具体实现方法如下: JavaScript实现文字与图片的拖拽效果 *{padding:0;margin:0;} . ...

  5. js整体缩小网页_妙用JavaScript实现网页的任意缩放

    现在网页上的字体是越来越小,别说是视力欠佳者就是好眼睛看久了也疼的难受,于是编写了下面这段小脚本,建议网页制作人能够加到网页代码的< head>中,以方便弱视人群放大浏览(仅适用于IE浏览 ...

  6. js中every用法_详解JavaScript中的every()方法

    JavaScript 数组中的每个方法测试数组中的所有元素是否经过所提供的函数来实现测试. 语法 array.every(callback[, thisObject]); 下面是参数的详细信息: ca ...

  7. js 数组去掉括号_如何删除Javascript数组中的方括号?

    我有一个名为value一个数组,当我console.log(value)我得到约30行代码有以下[6.443663, 3.419248]如何删除Javascript数组中的方括号? 的数字变化,因为它 ...

  8. java中的js是什么意思_什么是JavaScript

    来源:https://www.koofun.com/pro/kfpostsdetail?kfpostsid=30&cid= JavaScript是一种松散类型的客户端脚本语言,在用户浏览器中执 ...

  9. js点击获取—通过JS获取图片的相对坐标位置

    一.通过JS获取鼠标点击时图片的相对坐标位置 源代码如下所示: 1 <!DOCTYPE html> 2 <html lang="en"> 3 4 <h ...

  10. js进栈出栈_[js]数组栈和队列操作

    写在前面 在项目中,对数组的操作还是比较常见的,有时候,我们需要模拟栈和队列的特性才能实现需求,这里记录一下这个知识点. 栈 栈(stack)又名堆栈,它是一种运算受限的线性表.其限制是仅允许在表的一 ...

最新文章

  1. 好多Javascript日期选择器呀-6
  2. 笔试分享 | 带你解读校招人工智能笔试题
  3. 循环神经网络(recurrent neural network)(RNN)
  4. 随机过程及其在金融领域中的应用 第三章 习题 及 答案
  5. python必读_学好Python必读的几篇文章
  6. MTK:串口调试方法|MTK串口工具
  7. MongoDB安装、配置与示例
  8. 水果编曲软件除了做电音还能做什么
  9. php去掉省市区,PHP简单实现正则匹配省市区的方法
  10. SSM(Spring+SpringMVC+MyBatis)框架入门
  11. 【祥哥带你玩HoloLens开发】了解如何实现远程主机为HoloLens实时渲染
  12. 蓝桥杯【学做菜】Java
  13. 创办6年未盈利,半年亏损40亿裁员25%,狂奔的滴滴怎么了?
  14. 城市交通出行效率对比分析与思考
  15. 微信JSSDK使用签名算法
  16. win10 系统版本号获取的三种方法
  17. 打开excel服务器客户端无响应怎么办,excel服务器客户端登录不起
  18. yolo3+Mobilenetv1
  19. 使用达思SQL数据库修复软件导出数据库时的接收数据的数据库如何清空表数据?...
  20. zootracer使用说明——一款视频物体追踪软件,获取运动物体在屏幕坐标系的运动轨迹

热门文章

  1. [DirectX11]Gerstner波 实现简单的水面模拟
  2. PortAudio(v19) 在vs2010上的环境搭建
  3. Bzoj5251: [2018多省省队联测]劈配
  4. larvel nginx 配置
  5. HomeHack:黑客如何控制 LG 的 IoT 家用设备
  6. JavaScript权威设计--JavaScript表达式与运算符(简要学习笔记五)
  7. 【…感激2008,部署我的2009…】
  8. 设置ComboBox控件的提示内容.
  9. word受权限保护无法打开_保护S71500程序的几种方式
  10. MAC 安装brew raw.githubusercontent.com port 443: Connection refused 本人亲自认证过,踩过多种方案,最终认证的解决方案