文章目录
- [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
改造时)。即便这段代码不太好看,也要务必记得:在今后不得不使用对象的方法时,一定要先完成上下文的手动绑定,然后再将该方法作为无点式风格的一等对象进行传参(记住,我们的终极目标并不是纯粹的函数式编程,我们更推崇的是兼收并蓄其他有助于简化问题的构造)。
- Colossal Cave Adventure 是一款经典的文字冒险类游戏,最初由 Will Crowther 和 Don Woods 于 1976 年开发。它被认为是现代冒险游戏的开创者之一,影响了后来的许多游戏设计。 ↩︎