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

news/2024/10/8 12:33:08 标签: javascript, 开发语言, ecmascript

文章目录

  • 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 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 动作类型的函数代码来初始化一个对象。

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

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 处理逻辑:

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.niftyadmin.cn/n/5694130.html

相关文章

深度学习-----------------------注意力分数

目录 注意力分数注意力打分函数代码 掩蔽softmax操作拓展到高纬度Additive Attention(加性注意力)加性注意力代码演示一下AdditiveAttention类该部分总代码注意力权重 Scaled Dot-Product Attention(缩放点积注意力)缩放点积注意力代码演示一下DotProduc…

《Windows PE》4.1.4 手工重构导入表

接下来我们做一个稍微复杂一些的实验,实验需要四个程序: HelloWorld.exe:弹出MessageBox窗口(实验1已实现)。 Regedit.exe:添加注册表启动项。 LockTray.exe:锁定任务栏窗口。 UnLockTray.exe&…

用java编写飞机大战

游戏界面使用JFrame和JPanel构建。背景图通过BG类绘制。英雄机和敌机在界面上显示并移动。子弹从英雄机发射并在屏幕上移动。游戏有四种状态:READY、RUNNING、PAUSE、GAMEOVER。状态通过鼠标点击进行切换:点击开始游戏(从READY变为RUNNING&am…

Studying-多线程学习Part1-线程库的基本使用、线程函数中的数据未定义错误、互斥量解决多线程数据共享问题

来源:多线程编程 线程库的基本使用 两个概念: 进程是运行中的程序线程是进程中的进程 串行运行:一次只能取得一个任务并执行这一个任务 并行运行:可以同时通过多进程/多线程的方式取得多个任务,并以多进程或多线程…

黑马JavaWeb开发跟学(十二)SpringBootWeb案例

黑马JavaWeb开发跟学十二.SpringBootWeb案例 案例-登录认证1. 登录功能1.1 需求1.2 接口文档1.3 思路分析1.4 功能开发1.5 测试 2. 登录校验2.1 问题分析2.2 会话技术2.2.1 会话技术介绍2.2.2 会话跟踪方案2.2.2.1 方案一 - Cookie2.2.2.2 方案二 - Session2.2.2.3 方案三 - 令…

【分布式微服务云原生】Redis持久化策略:RDB vs AOF

Redis持久化策略:RDB vs AOF 摘要 本文深入探讨了Redis的两种主要持久化策略:RDB和AOF。我们将分析它们的工作原理、优缺点,并探讨如何在不同的应用场景中选择最合适的持久化策略。此外,文章还将提供Java代码示例和流程图&#…

828华为云征文 | 华为云Flexus X实例在混合云环境中的应用与实践

目录 前言 1. 混合云环境的优势与挑战 1.1 混合云的优势 1.2 混合云的挑战 2. Flexus X实例的配置与集成 2.1 Flexus X实例简介 2.2 Flexus X实例的混合云部署 2.3 配置步骤与措施 3. 数据迁移与同步策略 3.1 数据迁移方案 3.2 数据同步措施 4. 安全性与合规性管理…

性能剖析利器-Conan|得物技术

作者 / 得物技术 - 仁慈的狮子 目录 一、背景 1. 局限性 2. 向前一步 二、原理剖析 1. 系统架构 2. 工作模式 3. reporter 三、稳定性验证 四、案例分析 五、写在最后 一、背景 线上问题的定位与优化是程序员进阶的必经之路,常见的问题定位手段有日志排查、分布式链…