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

文章目录

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

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

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

js 复制代码
function xyzzy(...) { ... }

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

js 复制代码
var xyzzy = function(...) { ... }

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

拓展

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

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

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

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

js 复制代码
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 item
            return newState;

        case "DELETE":
            // update state, generating newState,
            // after deleting an item
            return newState;

        case "UPDATE":
            // update an item,
            // and generate an updated state
            return newState;

        default:
            return state;
    }
}

提示

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

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

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

js 复制代码
const dispatchTable = {
    CREATE: (state, action) => {
        // update state, generating newState,
        // depending on the action data
        // to create a new item
        return newState;
    },

    DELETE: (state, action) => {
        // update state, generating newState,
        // after deleting an item
        return newState;
    },

    UPDATE: (state, action) => {
        // update an item,
        // and generate an updated state
        return newState;
    }
};

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

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

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

2. 不必要的错误 An unnecessary mistake

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

js 复制代码
fetch("some/remote/url").then(function(data) {
    processResult(data);
});

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

拓展

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

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

js 复制代码
function someFunction(someData) { 
    return someOtherFunction(someData);
}

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

js 复制代码
fetch("some/remote/url").then(processResult);

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

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

知识拓展

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

3. 正确处理"方法" Working with methods

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

js 复制代码
fetch("some/remote/url").then(function(data) {
    myObject.store(data);
});

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

js 复制代码
fetch("some/remote/url").then(myObject.store);

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

js 复制代码
fetch("some/remote/url").then(myObject.store.bind(myObject));

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

js 复制代码
function doSomeMethod(someData) { 
    return someObject.someMethod(someData);
}

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

js 复制代码
const doSomeMethod = someObject.someMethod.bind(someObject);

小贴士

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

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


  1. Colossal Cave Adventure 是一款经典的文字冒险类游戏,最初由 Will CrowtherDon Woods 于 1976 年开发。它被认为是现代冒险游戏的开创者之一,影响了后来的许多游戏设计。 ↩︎
相关推荐
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
晓纪同学2 小时前
QT-简单视觉框架代码
开发语言·qt
威桑2 小时前
Qt SizePolicy详解:minimum 与 minimumExpanding 的区别
开发语言·qt·扩张策略
飞飞-躺着更舒服2 小时前
【QT】实现电子飞行显示器(简易版)
开发语言·qt
明月看潮生2 小时前
青少年编程与数学 02-004 Go语言Web编程 16课题、并发编程
开发语言·青少年编程·并发编程·编程与数学·goweb
明月看潮生2 小时前
青少年编程与数学 02-004 Go语言Web编程 17课题、静态文件
开发语言·青少年编程·编程与数学·goweb
Java Fans2 小时前
C# 中串口读取问题及解决方案
开发语言·c#
盛派网络小助手2 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#