我正在练习函数的部分应用,即修复函数参数。我学会了两种实现它的方法:
- 通过首先讨好原始函数。
- 通过使用
.bind()
方法。
在下面的例子中,我将展示只有第一个策略,即先讨好,有效。我的问题是为什么使用.bind()
不起作用。
例
请考虑以下数据:
const genderAndWeight = {
john: {
male: 100,
},
amanda: {
female: 88,
},
rachel: {
female: 73,
},
david: {
male: 120,
},
};
我想创建两个实用程序函数,将这些数据重新格式化为新对象:
- 函数 A -- 将人名作为键返回,权重作为值返回
- 函数 B -- 将人名作为键返回,将性别作为值返回
因为这两个函数应该非常相似,所以我想创建一个主函数,然后从中派生出两个版本,从而遵循 DRY 原则。
// master function
const getGenderOrWeightCurried = (fn) => (obj) =>
Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)]));
此解决方案的核心是我将提供给fn
参数的内容。所以要么
const funcA = (x) => Number(Object.values(x)); // will extract the weights
或
const funcB = (x) => Object.keys(x).toString(); // will extract the genders
现在做部分应用:
const getWeight = getGenderOrWeightCurried(funcA);
const getGender = getGenderOrWeightCurried(funcB);
效果很好:
console.log({
weight: getWeight(genderAndWeight),
gender: getGender(genderAndWeight),
});
// { weight: { john: 100, amanda: 88, rachel: 73, david: 120 },
// gender:
// { john: 'male',
// amanda: 'female',
// rachel: 'female',
// david: 'male' } }
<小时 />到目前为止一切顺利。以下方式使用.bind()
但不起作用
// master function
const getGenderOrWeightBothParams = (fn, obj) =>
Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)]));
// same as before
const funcA = (x) => Number(Object.values(x));
const funcB = (x) => Object.keys(x).toString();
// partial application using .bind()
const getWeight2 = getGenderOrWeightBothParams.bind(funcA, null);
const getGender2 = getGenderOrWeightBothParams.bind(funcB, null);
// log it out to console
console.log({weight: getWeight2(genderAndWeight), gender: getGender2(genderAndWeight)})
类型错误:fn 不是函数
值得注意的是,在不同的场景中,.bind()
确实允许部分应用。例如:
const mySum = (x, y) => x + y;
const succ = mySum.bind(null, 1);
console.log(succ(3)); // => 4
它来自哪里
咖喱和部分应用是功能遗产,因此在此上下文之外使用它们将阻止您获得它们的全部好处,并可能成为自我造成的混乱的根源。
提出的数据结构充满了问题,最大的问题是数据在数据对象的值和键中混合。姓名、性别和权重都是值。name
、gender
和weight
是键。这会将您的数据更改为此合理形状,其中它也采用合理的名称people
.
咖喱
pick
很容易实现它的目标,因为name
、gender
和weight
在语义上都是相邻的,也就是说,它们都是从对象中挑选的关键。当数据跨值和键混合时,会使导航结构变得更加困难,并为程序带来不必要的复杂性。
const people = [
{ name: "john", gender: "male", weight: 100 },
{ name: "amanda", gender: "female", weight: 88 },
{ name: "rachel", gender: "female", weight: 73 },
{ name: "david", gender: "male", weight: 120 }
]
// curried
const pick = (fields = []) => (from = []) =>
from.map(item => Object.fromEntries(fields.map(f => [f, item[f]])))
const nameAndGender =
pick(["name", "gender"]) // ✅ apply one argument
const nameAndWeight =
pick(["name", "weight"]) // ✅ apply one argument
console.log(nameAndGender(people))
console.log(nameAndWeight(people))
.as-console-wrapper { min-height: 100%; top: 0; }
部分应用
在这一点上,partial
完全足以促进您的理解。你不需要.bind
因为它的第一个参数与动态上下文有关,这是面向对象风格的原则。
这是使用非柯里pick
并应用partial
应用程序的相同演示 -
const people = [
{ name: "john", gender: "male", weight: 100 },
{ name: "amanda", gender: "female", weight: 88 },
{ name: "rachel", gender: "female", weight: 73 },
{ name: "david", gender: "male", weight: 120 }
]
// uncurried
const pick = (fields = [], from = []) =>
from.map(item => Object.fromEntries(fields.map(f => [f, item[f]])))
const partial = (f, ...a) =>
(...b) => f(...a, ...b)
const nameAndGender =
partial(pick, ["name", "gender"]) // ✅ partial application
const nameAndWeight =
partial(pick, ["name", "weight"]) // ✅ partial application
console.log(nameAndGender(people))
console.log(nameAndWeight(people))
.as-console-wrapper { min-height: 100%; top: 0; }
"是否必须更改数据结构?">
当然不是,但你很快就会遇到麻烦。让我们进行您的练习,看看问题出现在哪里。正如您所演示的,柯里程序工作正常 -
const genderAndWeight = {
john: {male: 100},
amanda: {female: 88},
rachel: {female: 73},
david: {male: 120},
}
const getGenderOrWeightCurried = (fn) => (obj) =>
Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)]));
const funcA = (x) => Number(Object.values(x));
const funcB = (x) => Object.keys(x).toString();
const getWeight = getGenderOrWeightCurried(funcA);
const getGender = getGenderOrWeightCurried(funcB);
console.log({
weight: getWeight(genderAndWeight),
gender: getGender(genderAndWeight),
});
.as-console-wrapper { min-height: 100%; top: 0; }
您问题中的部分应用程序错误地使用了.bind
。上下文 (null
) 作为第二个位置传递,但.bind
期望这个参数在第一个位置 -
const getWeight2 =
getGenderOrWeightBothParams.bind(funcA, null); // ❌
const getWeight2 =
getGenderOrWeightBothParams.bind(null, funcA); // ✅
您可以做同样的事情来修复getGender2
,但让我们改用partial
。动态上下文是一种面向对象的机制,当您学习函数式编程的基础知识时,您无需关注它。partial
允许您绑定函数的参数而无需提供上下文 -
const partial = (f, ...a) =>
(...b) => f(...a, ...b)
const getGender2 =
getGenderOrWeightBothParams.bind(funcB, null); // ❌
const gender2 =
partial(getGenderOrWeightBothParams, funcB); // ✅
这为您提供了使用原始建议的数据结构进行部分应用的两个工作示例 -
const genderAndWeight = {
john: {male: 100},
amanda: {female: 88},
rachel: {female: 73},
david: {male: 120},
}
const partial = (f, ...a) =>
(...b) => f(...a, ...b)
const getGenderOrWeightBothParams = (fn, obj) =>
Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)]));
const funcA = (x) => Number(Object.values(x));
const funcB = (x) => Object.keys(x).toString();
const getWeight2 =
getGenderOrWeightBothParams.bind(null, funcA); // ✅ .bind
const getGender2 =
partial(getGenderOrWeightBothParams, funcB) // ✅ partial
console.log({
weight: getWeight2(genderAndWeight),
gender: getGender2(genderAndWeight),
});
.as-console-wrapper { min-height: 100%; top: 0; }
"那问题出在哪里?">
就在这儿-
const funcA = (x) => Number(Object.values(x)); // ⚠️
const funcB = (x) => Object.keys(x).toString(); // ⚠️
"可是行!">
您是否知道您的funcA
会创建一个数字数组,将其转换为字符串,然后再次转换为数字?事实上,它看起来正常工作的唯一原因是因为每个人都是一个具有单个键/值对的对象。一旦添加更多条目,模型就会中断 -
const o1 = { female: 73 }
const o2 = { female: 73, accounting: 46000 }
const o3 = { gender: "female", weight: 73, role: "accounting", salary: 46000 }
const funcA = x => Number(Object.values(x))
console.log(funcA(o1)) // 73
console.log(funcA(o2)) // NaN
console.log(funcA(o3)) // NaN
类似的问题也发生在funcB
.您的函数似乎工作正常,因为单个字符串的数组["foo"]
转换为字符串时,将导致"foo"
。在任何更大的数组上尝试此操作,您将获得无法使用的结果 -
const o1 = { female: 73 }
const o2 = { female: 73, accounting: 46000 }
const o3 = { gender: "female", weight: 73, role: "accounting", salary: 46000 }
const funcB = x => Object.keys(x).toString()
console.log(funcB(o1)) // "female"
console.log(funcB(o2)) // "female,accounting"
console.log(funcB(o3)) // "gender,weight,role,salary"
当更多数据添加到树中时,funcA
和funcB
将如何工作?
下地狱再回来
我们知道,funcA
在原始数据中每个项目调用一次。随机选择一个人,让我们看看当funcA
达到rachel
的值时会发生什么。到底有多糟糕?
Number(Object.values(x)) x := { female: 73 }
Number(value) value := [73]
当使用参数
value
调用Number时,将执行以下步骤:
- 如果存在
value
,则 ✅
- 让
prim
成为?ToNumeric(value
).✅- 如果Type(
prim
) 是 BigInt,则设n
(R(prim
))。 ❌- 否则,
n
prim
. ✅- 还
- 让
n
为+0。- 如果未定义新目标,则返回
n
。 ✅- 让
O
成为?OrdinaryCreateFromConstructor(NewTarget,"%Number.prototype%">,« [[NumberData]] »).- 将"
O.[[NumberData]]
"设置为n
。- 返回
O
.
ToNumeric(value) value := [73]
抽象操作ToNumeric采用参数
value
并返回包含 Number 或 BigInt 的正常完成或抛出完成。它返回转换为数字或 BigIntvalue
。它在调用时执行以下步骤:
- 让
primValue
成为?ToPrimitive(value
,number).✅- 如果Type(
primValue
) 是 BigInt,则返回primValue
。 ❌- 返回?ToNumber(
primValue
)。✅
ToPrimitive(input[, preferredType]) input := [73], preferredType := number
抽象操作ToPrimitive接受参数
input
(ECMAScript 语言值)和可选参数preferredType
(字符串或数字),并返回包含 ECMAScript 语言值的正常补全或抛出补全。它将其input
参数转换为非 Object 类型。如果对象能够转换为多个基元类型,则可以使用可选的提示preferredType
来支持该类型。它在调用时执行以下步骤:
- 如果Type(
input
) 是 对象,则 ✅
- 让
exoticToPrim
成为?GetMethod(input
,@@toPrimitive).✅- 如果
exoticToPrim
不是未定义的,则 ❌
- 如果不存在
preferredType
,则提示为"默认"。- 否则,如果
preferredType
是字符串,则让提示为"字符串"。- 还
- 断言:
preferredType
是数字。- 让提示是"数字"。
- 让
result
成为?呼叫(exoticToPrim
,input
,« 提示 »).- 如果Type(
result
) 不是 Object,则返回result
。- 引发类型错误异常。
- 如果
preferredType
不存在,则preferredType
编号。 ❌- 返回?OrdinaryToPrimitive(
input
,preferredType
)。✅- 返回
input
. ✅
OrdinaryToPrimitive(O, hint) O := [73] hint := number
抽象操作OrdinaryToPrimitive接受参数
O
(对象)和hint
(字符串或数字),并返回包含 ECMAScript 语言值的正常完成或抛出完成。它在调用时执行以下步骤:
- 如果
hint
是字符串,则 ❌
- 让
methodNames
成为« "toString", "valueOf" »。- 还 ✅
- 让
methodNames
成为« "valueOf","toString" "。✅- 对于
methodNames
的每个元素name
,执行 ✅
- 让
method
成为?获取(O
,name
)。✅- 如果IsCallable(
method
) 为真,则 ✅
- 让结果是 ?呼叫(
method
,O
)。✅- 如果类型(结果)不是对象,则返回结果。 ⚠️
- 引发类型错误异常。
我们在这里越来越深,但我们几乎已经到达了博托姆。通过标记为 ⚠️ [[3.2.2]] 的点,数组的valueOf
将返回数组本身,该数组本身仍然具有 Object 类型。因此,循环 [[3.]] 继续name := "toString"
O := [73] name := "toString"
- 让
method
成为?获取(O
,name
)。✅- 如果IsCallable(
method
) 为真,则 ✅
- 让结果是 ?呼叫(
method
,O
)。✅- 如果类型(结果)不是对象,则返回结果。 ✅
OrdinaryToPrimitive(O, hint) O := [73] hint := number
Return => "73"
ToPrimitive(input[, preferredType]) input := [73], preferredType := number
Return => "73"
ToNumeric(value) value := [73]
Return => ToNumber("73")
ToNumber(argument) argument := "73"
抽象操作ToNumber采用参数
argument
并返回包含 Number 的正常完成或抛出完成。它将argument
转换为 Number 类型的值,如表 13(如下):
参数类型 结果 未定义的 返回NaN。 空 返回+0。 布尔值 如果 argument
为 true,则返回1。如果argument
为 false,则返回+0。号码 返回 argument
(无转换)。字符串 返回!StringToNumber( argument
)。✅符号 引发类型错误异常。 BigInt 抛出 typeError 异常。 对象 应用以下步骤: 。 1. 让 primValue
成为 ?ToPrimitive(argument
,number).。 2. 返回 ?ToNumber( primValue
)。