JavaScript 中函数返回其他函数的强大之处
在Medium上找到我
JavaScript 以其极高的灵活性而闻名。本文将展示一些利用函数优势的示例。
由于函数可以在任何地方传递,我们可以将它们传递到函数的参数中。
我第一次接触编程,是用 JavaScript 写代码,在实际操作中,有一个概念让我很困惑:如何将函数传递给其他函数。我尝试过一些所有高手都在做的“高级”操作,但最后总是得到这样的结果:
function getDate(callback) {
return callback(new Date())
}
function start(callback) {
return getDate(callback)
}
start(function (date) {
console.log(`Todays date: ${date}`)
})
这绝对是荒谬的,甚至让我们更难理解为什么在现实世界中我们会将函数传递给其他函数,而我们本可以这样做并得到相同的行为:
const date = new Date()
console.log(`Todays date: ${date}`)
getDate(callback)
但为什么这对于更复杂的情况来说不够好呢?除了感觉很酷之外,创建自定义函数并做额外的工作还有什么意义呢?
然后我继续询问有关这些用例的更多问题,并要求在社区论坛上给出一个良好使用的例子,但没有人愿意解释并举例说明。
现在回想起来,我意识到问题在于我的大脑还不懂如何用程序化的方式思考。要将思维从原来的生活转向用计算机语言编程,需要一段时间。
因为我理解试图理解高阶函数在 JavaScript 中何时有用的挫败感,所以我决定写这篇文章来逐步解释一个好的用例,从一个任何人都可以编写的非常基本的函数开始,然后从那里逐步进入一个提供额外好处的复杂实现。
有意图的功能
首先,我们将从旨在实现目标的功能开始。
那么,如果一个函数接受一个对象并返回一个新对象,以我们想要的方式更新样式,那会怎么样呢?
让我们使用这个对象(我们将其称为组件):
const component = {
type: 'label',
style: {
height: 250,
fontSize: 14,
fontWeight: 'bold',
textAlign: 'center',
},
}
我们希望让我们的函数保持height
不少于300
并将应用于border
按钮组件(带有的组件type: 'button'
)并将其返回给我们。
这看起来像这样:
function start(component) {
// Restrict it from displaying in a smaller size
if (component.style.height < 300) {
component.style['height'] = 300
}
if (component.type === 'button') {
// Give all button components a dashed teal border
component.style['border'] = '1px dashed teal'
}
if (component.type === 'input') {
if (component.inputType === 'email') {
// Uppercase every letter for email inputs
component.style.textTransform = 'uppercase'
}
}
return component
}
const component = {
type: 'div',
style: {
height: 250,
fontSize: 14,
fontWeight: 'bold',
textAlign: 'center',
},
}
const result = start(component)
console.log(result)
结果:
{
"type": "div",
"style": {
"height": 300,
"fontSize": 14,
"fontWeight": "bold",
"textAlign": "center"
}
}
假设我们想到了一个主意,每个组件都可以在其属性中放置更多组件children
。这意味着我们也必须让它处理内部组件。
因此,给定这样的组件:
{
"type": "div",
"style": {
"height": 300,
"fontSize": 14,
"fontWeight": "bold",
"textAlign": "center"
},
"children": [
{
"type": "input",
"inputType": "email",
"placeholder": "Enter your email",
"style": {
"border": "1px solid magenta",
"textTransform": "uppercase"
}
}
]
}
我们的函数显然还不能完成这项工作,但是:
function start(component) {
// Restrict it from displaying in a smaller size
if (component.style.height < 300) {
component.style['height'] = 300
}
if (component.type === 'button') {
// Give all button components a dashed teal border
component.style['border'] = '1px dashed teal'
}
if (component.type === 'input') {
if (component.inputType === 'email') {
// Uppercase every letter for email inputs
component.style.textTransform = 'uppercase'
}
}
return component
}
由于我们最近在组件中添加了子组件的概念,我们现在知道至少需要两种不同的方式来解决最终结果。现在正是开始思考抽象的好时机。将代码片段抽象为可复用的函数,可以提高代码的可读性和可维护性,因为它可以避免一些棘手的情况,例如调试某些实现细节中的问题。
当我们从某物中抽象出小部分时,开始思考如何将这些部分组合在一起也是一个好主意,我们可以将其称为组合。
抽象与组合
要知道要抽象什么,想想我们的最终目标是什么:
"A function that will take an object and return a new object that updated the styles on it the way we want it to"
本质上,这个函数的重点是将值转换为我们期望的表示形式。记住,我们最初的函数是转换组件的样式,但后来我们还添加了组件本身,可以通过其children
属性包含组件。因此,我们可以从抽象这两部分开始,因为很有可能在更多情况下,我们需要创建更多函数来对值执行类似的操作。在本教程中,可以将这些抽象函数称为解析器 (resolvers):
function resolveStyles(component) {
// Restrict it from displaying in a smaller size
if (component.style.height < 300) {
component.style['height'] = 300
}
if (component.type === 'button') {
// Give all button components a dashed teal border
component.style['border'] = '1px dashed teal'
}
if (component.type === 'input') {
if (component.inputType === 'email') {
// Uppercase every letter for email inputs
component.style.textTransform = 'uppercase'
}
}
return component
}
function resolveChildren(component) {
if (Array.isArray(component.children)) {
component.children = component.children.map((child) => {
// resolveStyles's return value is a component, so we can use the return value from resolveStyles to be the the result for child components
return resolveStyles(child)
})
}
return component
}
function start(component, resolvers = []) {
return resolvers.reduce((acc, resolve) => {
return resolve(acc)
}, component)
}
const component = {
type: 'div',
style: {
height: 250,
fontSize: 14,
fontWeight: 'bold',
textAlign: 'center',
},
children: [
{
type: 'input',
inputType: 'email',
placeholder: 'Enter your email',
style: {
border: '1px solid magenta',
},
},
],
}
const result = start(component, [resolveStyles, resolveChildren])
console.log(result)
结果:
{
"type": "div",
"style": {
"height": 300,
"fontSize": 14,
"fontWeight": "bold",
"textAlign": "center"
},
"children": [
{
"type": "input",
"inputType": "email",
"placeholder": "Enter your email",
"style": {
"border": "1px solid magenta",
"textTransform": "uppercase"
}
}
]
}
重大变更
接下来让我们讨论一下此代码如何导致灾难性错误——会导致您的应用程序崩溃的错误。
如果我们仔细观察解析器并了解它们如何用于计算最终结果,我们会发现它很容易中断并导致我们的应用程序崩溃,原因有二:
- 它会变异——如果发生未知错误,错误地将未定义的值赋给该值,从而导致值变异,该怎么办?由于变异,函数外部的值也会波动(了解引用的工作原理)。
如果我们从中取出return component
,resolveStyles
我们会立即遇到一个,TypeError
因为这将成为下一个解析器函数的传入值:
TypeError: Cannot read property 'children' of undefined
- 解析器会覆盖先前的结果——这不是一个好的做法,也违背了抽象的目的。我们可以计算它的值,但如果函数返回一个全新的值,
resolveStyles
那就无关紧要了。resolveChildren
保持事物不变
通过使这些函数不可变并确保它们在给定相同值时始终返回相同的结果,我们可以安全地实现我们的目标。
合并新的变更
在函数内部,resolveStyles
我们可以返回一个新值(对象),其中包含将与原始值合并的更改值。这样,我们可以确保解析器不会相互覆盖,并且返回值undefined
对之后的其他代码不会产生任何影响:
function resolveStyles(component) {
let result = {}
// Restrict it from displaying in a smaller size
if (component.style.height < 300) {
result['height'] = 300
}
if (component.type === 'button') {
// Give all button components a dashed teal border
result['border'] = '1px dashed teal'
}
if (component.type === 'input') {
if (component.inputType === 'email') {
// Uppercase every letter for email inputs
result['textTransform'] = 'uppercase'
}
}
return result
}
function resolveChildren(component) {
if (Array.isArray(component.children)) {
return {
children: component.children.map((child) => {
return resolveStyles(child)
}),
}
}
}
function start(component, resolvers = []) {
return resolvers.reduce((acc, resolve) => {
return resolve(acc)
}, component)
}
当项目变得更大时
如果我们有 10 种样式解析器,但只有 1 种解析器用于子解析器,那么维护起来会变得困难,因此我们可以在合并的部分将它们拆分:
function callResolvers(component, resolvers) {
let result
for (let index = 0; index < resolvers.length; index++) {
const resolver = resolvers[index]
const resolved = resolver(component)
if (resolved) {
result = { ...result, ...resolved }
}
}
return result
}
function start(component, resolvers = []) {
let baseResolvers
let styleResolvers
// Ensure base resolvers is the correct data type
if (Array.isArray(resolvers.base)) baseResolvers = resolvers.base
else baseResolvers = [resolvers.base]
// Ensure style resolvers is the correct data type
if (Array.isArray(resolvers.styles)) styleResolvers = resolvers.styles
else styleResolvers = [resolvers.styles]
return {
...component,
...callResolvers(component, baseResolvers),
style: {
...component.style,
...callResolvers(component, styleResolvers)),
},
}
}
调用这些解析器的代码已被抽象为其自己的函数,以便我们可以重复使用它并减少重复。
如果我们有一个解析器需要更多的上下文来计算其结果怎么办?
例如,如果我们有一个resolveTimestampInjection
解析器函数,time
当使用包装器中某处传递的某些选项参数时,它会注入一个属性,该怎么办?
需要附加上下文的函数
如果解析器能够获取额外的上下文,而不仅仅是接收component
值作为参数,那就太好了。我们可以通过在解析器函数中使用第二个参数来实现这个功能,但我认为这些参数应该放在组件层面的底层抽象中。
如果解析器能够返回一个函数并从返回的函数的参数中接收所需的上下文,那会怎样?
看起来像这样:
function resolveTimestampInjection(component) {
return function ({ displayTimestamp }) {
if (displayTimestamp === true) {
return {
time: new Date(currentDate).toLocaleTimeString(),
}
}
}
}
如果我们可以在不改变原始代码行为的情况下启用此功能,那就太好了:
function callResolvers(component, resolvers) {
let result
for (let index = 0; index < resolvers.length; index++) {
const resolver = resolvers[index]
const resolved = resolver(component)
if (resolved) {
result = { ...result, ...resolved }
}
}
return result
}
function start(component, resolvers = []) {
let baseResolvers
let styleResolvers
// Ensure base resolvers is the correct data type
if (Array.isArray(resolvers.base)) baseResolvers = resolvers.base
else baseResolvers = [resolvers.base]
// Ensure style resolvers is the correct data type
if (Array.isArray(resolvers.styles)) styleResolvers = resolvers.styles
else styleResolvers = [resolvers.styles]
return {
...component,
...callResolvers(component, baseResolvers),
style: {
...component.style,
...callResolvers(component, styleResolvers)),
},
}
}
const component = {
type: 'div',
style: {
height: 250,
fontSize: 14,
fontWeight: 'bold',
textAlign: 'center',
},
children: [
{
type: 'input',
inputType: 'email',
placeholder: 'Enter your email',
style: {
border: '1px solid magenta',
},
},
],
}
const result = start(component, {
resolvers: {
base: [resolveTimestampInjection, resolveChildren],
styles: [resolveStyles],
},
})
这就是组合高阶函数的力量开始显现的地方,好消息是它们很容易实现!
抽象化抽象
为了实现此功能,让我们将抽象提升一步,将解析器包装到负责将上下文注入低级解析器函数的高阶函数中。
function makeInjectContext(context) {
return function (callback) {
return function (...args) {
let result = callback(...args)
if (typeof result === 'function') {
// Call it again and inject additional options
result = result(context)
}
return result
}
}
}
我们现在可以从注册为解析器的任何函数返回一个函数,并且仍然保持应用程序的行为相同,如下所示:
const getBaseStyles = () => ({ baseStyles: { color: '#333' } })
const baseStyles = getBaseStyles()
const injectContext = makeInjectContext({
baseStyles,
})
function resolveTimestampInjection(component) {
return function ({ displayTimestamp }) {
if (displayTimestamp === true) {
return {
time: new Date(currentDate).toLocaleTimeString(),
}
}
}
}
在我展示最后一个例子之前,让我们先来看看makeInjectContext
高阶函数以及它的作用:
它首先获取一个要传递给所有解析器函数的对象,然后返回一个以回调函数作为参数的函数。这个回调参数稍后将成为原始解析器函数之一。我们这样做的原因是我们正在进行所谓的包装。我们用外部函数包装了回调,以便我们可以注入额外的功能,同时仍然通过确保在这里调用回调来保持原始函数的行为。如果回调结果的返回类型是一个函数,我们将假定回调需要上下文,因此我们再次调用回调的结果- 这就是我们传入上下文的地方。
当我们调用该回调(调用者提供的函数)并在包装函数内部进行一些计算时,我们会获得来自包装函数和调用者的值。这对于我们的最终目标来说是一个很好的用例,因为我们希望将结果合并在一起,而不是让每个解析函数都能够覆盖前一个解析函数的值或结果!值得一提的是,还有其他高级用例可以解决不同的问题,而这是一个很好的例子,它展示了我们需要根据情况采用正确策略的情况——因为如果你像我一样,每次看到机会时,你可能会尝试实现很多高级用例——这是一种不好的做法,因为根据具体情况,某些高级模式确实比其他模式更好!
现在我们的start
函数需要调整为makeInjectContext
高阶函数:
const getBaseStyles = () => ({ baseStyles: { color: '#333' } })
function start(component, { resolvers = {}, displayTimestamp }) {
const baseStyles = getBaseStyles()
// This is what will be injected in the returned function from the higher order function
const context = { baseStyles, displayTimestamp }
// This will replace each original resolver and maintain the behavior of the program to behave the same by calling the original resolver inside it
const enhancedResolve = makeInjectContext(context)
let baseResolvers
let styleResolvers
// Ensure base resolvers is the correct data type
if (Array.isArray(resolvers.base)) baseResolvers = resolvers.base
else baseResolvers = [resolvers.base]
// Ensure style resolvers is the correct data type
if (Array.isArray(resolvers.styles)) styleResolvers = resolvers.styles
else styleResolvers = [resolvers.styles]
return {
...component,
...callResolvers(component, baseResolvers.map(enhancedResolve)),
style: {
...component.style,
...callResolvers(component, styleResolvers.map(enhancedResolve)),
},
}
}
const component = {
type: 'div',
style: {
height: 250,
fontSize: 14,
fontWeight: 'bold',
textAlign: 'center',
},
children: [
{
type: 'input',
inputType: 'email',
placeholder: 'Enter your email',
style: {
border: '1px solid magenta',
},
},
],
}
const result = start(component, {
resolvers: {
base: [resolveTimestampInjection, resolveChildren],
styles: [resolveStyles],
},
})
我们仍然会得到一个具有预期结果的对象!
{
"type": "div",
"style": {
"height": 300,
"fontSize": 14,
"fontWeight": "bold",
"textAlign": "center"
},
"children": [
{
"type": "input",
"inputType": "email",
"placeholder": "Enter your email",
"style": {
"border": "1px solid magenta"
},
"textTransform": "uppercase"
}
],
"time": "2:06:16 PM"
}
结论
这篇文章到此结束!希望你觉得这篇文章很有价值,并期待未来有更多精彩内容!
在Medium上找到我
鏂囩珷鏉ユ簮锛�https://dev.to/jsmanifest/the-power-of-functions-returning-other-functions-in-javascript-11lo