这不是关于嵌套函数,而是关于 schönfinkeling / currying。 Schönfinkeling / currying 以开发该技术的 Moses Schönfinkel 命名(在 Gottlob Frege 之前介绍过该技术之后)和完善并描述该技术的 Haskell Curry。
简单来说,柯里化是一种允许将任何 n 个参数的函数转换为 n-1 个参数的函数的技术,该函数返回一个接受 n-1 个参数的函数em>nth 参数。通过反复应用这一点,您可以证明您永远不需要具有多个参数的函数来对具有任意多个参数的函数进行建模。
这是一个例子。我可以打开一个添加两个数字的函数:
function add(a, b) { return a + b; }
add(2, 3)
//=> 5
进入一个返回加法器函数的“加法器工厂”,当调用它时将产生两个数字的总和:
function adderFactory(a) {
return function adder(b) { return a + b; };
}
const twoAdder = adderFactory(2);
twoAdder(3)
//=> 5
或
adderFactory(2)(3)
//=> 5
现在,您可能会想:“但是 ECMAScript 支持具有多个参数的函数,那么我为什么要使用柯里化来模拟它们,如果我可以在本地拥有它们呢?”你是对的!由于这个原因,使用柯里化是没有意义的。
但是,您可能还想用函数做另一件有趣的事情:部分应用程序。 “函数应用”只是“调用函数”的函数式编程,因此“部分应用”意味着“仅使用其参数的子集调用函数”。部分应用程序调用一个仅带有一些参数的函数,并生成一个仅针对这些参数的特化 函数。在支持部分应用的语言中,我可以这样做:
const fourAdder = add(4, ?);
但是,ECMAScript 没有部分应用。
但是,当我对我的函数进行 curry 时,我可以进行“某种部分应用”,我至少可以只提供前几个参数,而忽略最后几个参数。这意味着您必须考虑哪些参数更可能是固定的,哪些参数更可能是可变的,并且应该按“可变性”对它们进行排序。
因此,对于您发布的函数,可以创建一个碱基转换器,它只能将一个特定数字从一个特定的碱基转换为任意数量的碱基。我必须承认,这实际上并不是非常有用。如果函数这样定义会更有用:
const convertFromBaseToBase = baseFrom =>
baseTo =>
num => parseInt(num, baseFrom).toString(baseTo);
convertFromBaseToBase(2)(8)('1001')
//=> '11'
现在,例如,您可以像这样创建一个从八进制到十六进制的转换器:
const octalToHexadecimalConverter = convertFromBaseToBase(8)(16);
octalToHexadecimalConverter('17')
//=> "F"
注意!由于您只能“从右侧部分应用”的限制,您实际上也可以使用带有默认参数的可选参数来执行此操作,有点像这样:
const baseToToken = Symbol('baseTo'),
numToken = Symbol('num');
function convertFromBaseToBase(baseFrom, baseTo=baseToToken, num=numToken) {
if (num === numToken) {
if (baseTo === baseToToken) {
return (baseTo, num=numToken) =>
num === numToken ?
num => parseInt(num, baseFrom).toString(baseTo) :
parseInt(num, baseFrom).toString(baseTo);
} else {
return num => parseInt(num, baseFrom).toString(baseTo);
}
} else {
return parseInt(num, baseFrom).toString(baseTo);
}
}
convertFromBaseToBase(8, 16, '17')
//=> 'F'
convertFromBaseToBase(8, 16)('17')
//=> 'F'
convertFromBaseToBase(8)(16)('17')
//=> 'F'
convertFromBaseToBase(8)(16, '17')
//=> 'F'
但是,正如你所看到的,这开始变得非常丑陋,非常快。
问题中的 sn-p 还有一个原因:它提供了一个 fluent 接口,它为特定参数提供名称,这样您就不会混淆两个数字参数baseFrom和baseTo。然而,这也可以通过其他几种方式来解决。一种是通过命名函数,以便清楚地知道是 baseFrom 还是 baseTo 先出现,即,而不是 convertBase(num, baseFrom, baseTo) 将其称为 convertNumberFromBaseToBase(num, baseFrom, baseTo)。另一种可能性是使用对象参数,如下所示:
function convertBase({ num, baseFrom, baseTo }) {
return parseInt(num, baseFrom).toString(baseTo);
}
convertBase({ num: '17', baseFrom: 8, baseTo: 16 })
//=> 'F'
但是,即使使用更具描述性的函数名称或流畅的接口,改变参数的顺序仍然是有意义的,以使柯里化和部分应用更有用。
还请注意,对于嵌套函数不用于柯里化,我根本没有说过任何话,例如[代码改编自Ruby Recursive Indexing/Searching Method (Using Middle Comparison) Returning Incorrect Index Value]:
function bsearch(arr, target) {
function bsearchRec(arr, target, offset=0) {
const middleIndex = Math.floor(arr.length / 2);
if (arr[middleIndex] === target) { return offset + middleIndex; }
if (arr.length === 1) { return undefined; }
if (target > arr[middleIndex]) {
return bsearchRec(arr.slice(middleIndex+1), target, offset + middleIndex + 1);
} else if (target < arr[middleIndex]) {
return bsearchRec(arr.slice(0, middleIndex), target, offset);
}
}
return bsearchRec(arr, target);
}
bsearch([1, 3, 4, 5, 9], 5)
//=> 3
这里,嵌套函数 bsearchRec 嵌套在 bsearch 内部,因为它是 bsearch 的私有内部实现细节,除了 bsearch 的作者之外没有人应该知道它。
最后,函数是 ECMAScript 中用于封装的工具。特别是,函数是 ECMAScript 实现对象的方式。对象具有由名称和封装标识的行为。在大多数 OO 语言中,行为、封装和名称到行为的映射(也称为“方法调用”)这三件事是由一个实体提供的,即对象。在 ECMAScript 中,封装由函数(闭包)提供,行为由函数提供(嵌套在闭包内以共享私有状态),从名称到行为的映射由字典提供,字典被混淆地称为对象,尽管它们只实现了对象含义的三分之一。
因此,没有嵌套函数,ECMAScript 中就没有封装,最重要的是,没有对象!甚至模块和类也主要是嵌套函数之上的语法糖。