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

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 年开发。它被认为是现代冒险游戏的开创者之一,影响了后来的许多游戏设计。 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安冬的码畜日常

您的鼓励是我持续优质内容的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值