为什么部分应用程序在柯里时有效,但使用 .bind() 时不起作用



我正在练习函数的部分应用,即修复函数参数。我学会了两种实现它的方法:

  1. 通过首先讨好原始函数。
  2. 通过使用.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

它来自哪里

咖喱和部分应用是功能遗产,因此在此上下文之外使用它们将阻止您获得它们的全部好处,并可能成为自我造成的混乱的根源。

提出的数据结构充满了问题,最大的问题是数据在数据对象的值键中混合。姓名、性别和权重都是namegenderweight是键。这会将您的数据更改为此合理形状,其中它也采用合理的名称people.

咖喱

pick很容易实现它的目标,因为namegenderweight在语义上都是相邻的,也就是说,它们都是从对象中挑选的关键。当数据跨值和键混合时,会使导航结构变得更加困难,并为程序带来不必要的复杂性。

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"

当更多数据添加到树中时,funcAfuncB将如何工作?

下地狱再回来

我们知道,funcA在原始数据中每个项目调用一次。随机选择一个人,让我们看看当funcA达到rachel的值时会发生什么。到底有多糟糕?

Number(Object.values(x))  x := { female: 73 }
Number(value)  value := [73]

当使用参数value调用Number时,将执行以下步骤:

  1. 如果存在value,则 ✅
    1. prim成为?ToNumeric(value).✅
    2. 如果Type(prim) 是 BigInt,则设n(R(prim))。 ❌
    3. 否则,nprim. ✅
    1. n+0
  2. 如果未定义新目标,则返回n。 ✅
  3. O成为?OrdinaryCreateFromConstructor(NewTarget"%Number.prototype%">« [[NumberData]] »).
  4. 将"O.[[NumberData]]"设置为n
  5. 返回O.
ToNumeric(value)  value := [73]

抽象操作ToNumeric采用参数value并返回包含 Number 或 BigInt 的正常完成或抛出完成。它返回转换为数字或 BigIntvalue。它在调用时执行以下步骤:

  1. primValue成为?ToPrimitive(valuenumber).✅
  2. 如果Type(primValue) 是 BigInt,则返回primValue。 ❌
  3. 返回?ToNumber(primValue)。✅
ToPrimitive(input[, preferredType])  input := [73], preferredType := number

抽象操作ToPrimitive接受参数input(ECMAScript 语言值)和可选参数preferredType(字符串数字),并返回包含 ECMAScript 语言值的正常补全或抛出补全。它将其input参数转换为非 Object 类型。如果对象能够转换为多个基元类型,则可以使用可选的提示preferredType来支持该类型。它在调用时执行以下步骤:

  1. 如果Type(input) 是 对象,则 ✅
    1. exoticToPrim成为?GetMethod(input@@toPrimitive).✅
    2. 如果exoticToPrim不是未定义的,则 ❌
      1. 如果不存在preferredType,则提示为"默认"。
      2. 否则,如果preferredType字符串,则让提示为"字符串"。
        1. 断言:preferredType数字
        2. 让提示是"数字"。
      3. result成为?呼叫(exoticToPriminput« 提示 »).
      4. 如果Type(result) 不是 Object,则返回result
      5. 引发类型错误异常。
    3. 如果preferredType不存在,则preferredType编号。 ❌
    4. 返回?OrdinaryToPrimitive(inputpreferredType)。✅
  2. 返回input. ✅
OrdinaryToPrimitive(O, hint)  O := [73]  hint := number

抽象操作OrdinaryToPrimitive接受参数O(对象)和hint(字符串数字),并返回包含 ECMAScript 语言值的正常完成或抛出完成。它在调用时执行以下步骤:

  1. 如果hint字符串,则 ❌
    1. methodNames成为« "toString", "valueOf" »
  2. 还 ✅
    1. methodNames成为« "valueOf","toString" "。
  3. 对于methodNames的每个元素name,执行 ✅
    1. method成为?获取(Oname)。✅
    2. 如果IsCallable(method) 为,则 ✅
      1. 结果是 ?呼叫(methodO)。✅
      2. 如果类型(结果)不是对象,则返回结果。 ⚠️
  4. 引发类型错误异常。

我们在这里越来越深,但我们几乎已经到达了博托姆。通过标记为 ⚠️ [[3.2.2]] 的点,数组的valueOf将返回数组本身,该数组本身仍然具有 Object 类型。因此,循环 [[3.]] 继续name := "toString"

O := [73]  name := "toString"
  1. method成为?获取(Oname)。✅
  2. 如果IsCallable(method) 为,则 ✅
    1. 结果是 ?呼叫(methodO)。✅
    2. 如果类型(结果)不是对象,则返回结果。 ✅
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(argumentnumber).
2. 返回 ?ToNumber(primValue)。