【玩转 JS 函数式编程_009】3.1.3 JavaScript 函数式编程筑基之:将函数视为一等对象

news/2024/12/22 11:30:26/

文章目录

  • 3.1.3. 将函数用作对象 Functions as objects
    • 1. React-Redux 中的 reducer(A React-Redux reducer)
    • 2. 不必要的错误 An unnecessary mistake
    • 3. 正确处理“方法” Working with methods

3.1.3. 将函数用作对象 Functions as objects

所谓一等对象,是指函数本身可以诸如数字或字符串那样,被创建、赋值、变更、被传入参数,以及像其他数据类型那样被其它函数作为返回值返回。先来看看函数的常规定义方式:

function xyzzy(...) { ... }

这类声明与下面的语句大抵等效:

var xyzzy = function(...) { ... }

这里的“等效”对变量提升效应不适用。变量提升效应只提升变量的 声明部分 而非 赋值部分 到当前作用域顶部。因此,对于第一个定义,函数调用可以在代码任一位置生效;而第二个定义中,函数调用只在该赋值语句执行后生效。

拓展

发现与游戏 巨穴冒险(Colossal Cave Adventure) 1 的相似之处了吗?在任何地方调用 xyzzy(...) 并不总是有效!如果您还从未玩过那个著名的互动虚拟游戏,不妨在线试试——访问 地址1 或 地址2 即可。

这里想说明的点在于函数可以被赋值给一个变量,如果需要的话也可以重新赋值。类似地,我们可以在需要时临时定义函数。甚至可以不对函数命名:与普通表达式一样,如果只调用一次,则无需命名或赋值给一个变量。

1. React-Redux 中的 reducer(A React-Redux reducer)

来看另一个关于函数赋值的示例。如前所述,React-Redux 的工作原理是分发由 reducer 处理的 action 操作对象。 通常 reducer 含有一段像这样的带开关的代码:

function doAction(state = initialState, action) {let newState = {};switch (action.type) {case "CREATE":// update state, generating newState,// depending on the action data// to create a new itemreturn newState;case "DELETE":// update state, generating newState,// after deleting an itemreturn newState;case "UPDATE":// update an item,// and generate an updated statereturn newState;default:return state;}
}

提示

initialState 作为 state 的默认值,是首次初始化全局状态时的简单处理手法。别去死盯着那个默认值, 它与本例演示的重点无关,这里只是为了要素完整起见而引入的。

利用存储函数的可能性,不妨构建一个 调度表 来简化上述代码。首先,使用每个 action 动作类型的函数代码来初始化一个对象。

基本上,我们只是采用前面的代码创建出单独的函数:

const dispatchTable = {CREATE: (state, action) => {// update state, generating newState,// depending on the action data// to create a new itemreturn newState;},DELETE: (state, action) => {// update state, generating newState,// after deleting an itemreturn newState;},UPDATE: (state, action) => {// update an item,// and generate an updated statereturn newState;}
};

我们将处理每类 action 的函数作为某对象的属性存到一个对象中,该对象即为我们需要的调度表。这个调度表对象只需要创建一次,就能在应用程序执行期间保持不变。这样就能用一行代码重写前面的 action 处理逻辑:

function doAction2(state = initialState, action) {return dispatchTable[action.type]? dispatchTable[action.type](state, action): state;
}

来仔细分析一下这段代码:给定一个 action,若 action.type 匹配到了调度表对象中的某一属性,则执行该属性对应的处理函数,返回一个新状态;否则只返回 Redux 所需的当前状态。如若不能将函数(存储及调用)作为一等对象处理,那么上述代码是无法满足需求的。

2. 不必要的错误 An unnecessary mistake

这里通常会出现一个常见的、无伤大雅的错误(虽然并无公害)。您可能经常看到像这样的代码:

fetch("some/remote/url").then(function(data) {processResult(data);
});

这段代码是做什么用的?大概意思是从某个远程 URL 获取到了结果后调用了一个函数。该函数又调用了 processResult 函数,并传入自身的参数(data)作为其参数。换言之,在 then() 的部分,我们需要一个函数,在给定 data 后,去执行 processResult(data)。但问题是,这样的函数不就是现成的么?

拓展

先上理论:在 Lambda 演算术语中,我们将【λx.func x】简单地替换为 func ——这称为 eta 转换eta 约简。(反之,则得到一个 eta 抽象)。本例可被认为是做了一次(非常非常小的)优化,其主要优点是写出更简短、更紧凑的代码。

一般的原则是,只要见到类似这样的代码:

function someFunction(someData) { return someOtherFunction(someData);
}

就可以考虑用 someOtherFunction 进行如下替换,将本例改写为:

fetch("some/remote/url").then(processResult);

这段代码完全等同于我们之前看到的回调逻辑(由于避免了一次函数调用,故而性能上有极细微的提升)。这样一来是否更容易理解呢?

这种编程风格被称为 无点式(pointfree) 风格或 默认(tacit) 风格,其主要特点是无需为每个函数的调用指定任何参数。这类编码方式的一个优点是帮助开发者(以及今后读到该代码的人)思考函数本身的含义,而不是费心于传参、调用这样的底层细节上。简化版本中并没有多余或不相关的调用细节:只要了解被调用函数的作用,就相当于了解了完整代码的含义。后续章节中我们还会经常(但不一定总是)见到这种写法的身影。

知识拓展

Unix / Linux 用户可能早就习惯了这种编码风格,因为当使用管道(pipes)将某命令的结果作为输入项传递给另一个命令时,就是以类似的方式工作的。执行命令 ls | grep doc | sort 时,ls 的输出是 grep 命令的输入,而后者的输出是 sort 的输入——但输入的参数不会显式地写出来;它们都是是隐含的。在第八章介绍无点式风格小节,我们还将继续探讨相关话题。

3. 正确处理“方法” Working with methods

还有一种情况值得关注:在调用一个对象的方法时,会发生什么?来看下面的代码:

fetch("some/remote/url").then(function(data) {myObject.store(data);
});

如果原代码与前述代码类似,那么看似显而易见的转换将出错:

fetch("some/remote/url").then(myObject.store);

什么原因呢?这是因为在原代码中,被调用的方法是绑定到一个对象(myObject)上的;而在转换后的代码中该方法并没有被绑定,它只是一个自由函数。要解决这个问题,可以使用 bind() 函数进行如下修复:

fetch("some/remote/url").then(myObject.store.bind(myObject));

这是一种通用的解决方案:移植某个方法时,不能只考虑赋值;还必须使用 bind() 绑定原方法中正确的上下文:

function doSomeMethod(someData) { return someObject.someMethod(someData);
}

按照这个转换规则,上述代码应该转换成下面的方式后,才能以无点式风格进行传参:

const doSomeMethod = someObject.someMethod.bind(someObject);

小贴士

更多 bind 介绍,详见 MDN 官方文档。

这样的写法看起来很蹩脚,也不甚优雅;但为了让方法关联到正确的对象上,也只能这样写了。在第六章中我们还将看到这一写法的具体应用(将函数作 promise 改造时)。即便这段代码不太好看,也要务必记得:在今后不得不使用对象的方法时,一定要先完成上下文的手动绑定,然后再将该方法作为无点式风格的一等对象进行传参(记住,我们的终极目标并不是纯粹的函数式编程,我们更推崇的是兼收并蓄其他有助于简化问题的构造)。


  1. Colossal Cave Adventure 是一款经典的文字冒险类游戏,最初由 Will CrowtherDon Woods 于 1976 年开发。它被认为是现代冒险游戏的开创者之一,影响了后来的许多游戏设计。 ↩︎


http://www.ppmy.cn/news/1536192.html

相关文章

使用前端三剑客实现一个备忘录

一,界面介绍 这个备忘录的界面效果如下: 可以实现任务的增删,并且在任务被勾选后会被放到已完成的下面。 示例: (1),增加一个任务 (2),勾选任务 &#xff…

计算机毕业设计 基于SpringBoot和Vue的课程教学平台的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍:✌从事软件开发10年之余,专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ 🍅文末获取源码联系🍅 👇🏻 精…

pod管理及优化

一、k8s中的资源 1、资源介绍 [rootk8s-master ~]# kubectl --namespace timinglee get po No resources found in timinglee namespace. [rootk8s-master ~]# kubectl run testpod --image timinglee/nginx [rootk8s-master ~]# kubectl get pods -w NAME READY STATU…

数据治理中场战事:按需采购和自如升级成必然

在过去的十余年间,中国的大数据产业犹如一场技术革命,以惊人的速度蓬勃发展。十年前,金融、电信等行业构建数据仓库时,几乎无一例外地依赖于国际厂商的产品与服务,许多项目的成本动辄耗费千万乃至上亿元。 于是&#…

MySQL 实验 7:索引的操作

MySQL 实验 7:索引的操作 索引是对数据表中一列或多列的值进行排序的一种结构,索引可以大大提高 MySQL 的检索速度。合理使用索引,可以大大提升 SQL 查询的性能。 索引好比是一本书前面的目录,假如我们需要从书籍查找与 xx 相关…

21.2 k8s中etcd的tls双向认证原理解析

本节重点介绍 : tls单向认证原理tls双向认证原理 在k8s中etcd监控的应用以ca.crt client.crt client.key创建的secret并挂载到prometheus中prometheus配置证书信息打到采集etcd的目的 tls单向认证 在单向SSL身份认证过程中,客户端需要验证服务端证书,…

[SpringBoot] 苍穹外卖--面试题总结--上

前言 1--苍穹外卖-SpringBoot项目介绍及环境搭建 详解-CSDN博客 2--苍穹外卖-SpringBoot项目中员工管理 详解(一)-CSDN博客 3--苍穹外卖-SpringBoot项目中员工管理 详解(二)-CSDN博客 4--苍穹外码-SpringBoot项目中分类管理 详…

Transformer--详解

Transformer旨在解决自然语言处理任务中的长依赖性问题。与传统的递归神经网络(如LSTM、GRU)不同,Transformer完全摒弃了递归结构,依赖自注意力机制(Self-Attention)来建模输入序列中的所有位置之间的关系。…