函数传参与控制反转
- # javascript
- # callback
- # design-pattern
在手搓 Promise 的文章里有这样一段代码:
class MyTask {
value
constructor(executor) {
const done = (value) => {
this.value = value
console.log('Done with value:', this.value)
}
const fail = (reason) => {
this.value = reason
console.log('Failed with reason:', this.value)
}
executor(done, fail)
}
}
使用时大概是这样:
new MyTask((done, fail) => {
done('hello')
})
这种写法不神秘:把一段函数传进去,内部准备好一些能力,再把这些能力交给外部函数使用。
你之前提到的几个叫法都能成立,只是它们站在不同层面看同一件事。
最常见的叫法:构造函数注入(Constructor Injection)
从代码形态看,最容易想到的是构造函数注入。
new MyTask((done, fail) => {
done('hello')
})
这里把一个函数传给构造函数:
class MyTask {
constructor(executor) {
executor(done, fail)
}
}
这个传进去的函数,可以叫:
- executor function(执行器函数)
- callback(回调函数)
- constructor callback(构造器回调)
不过这里和传统依赖注入里的 constructor injection 还有一点区别。
常见的构造函数注入,通常是把一个依赖对象传进来:
class UserService {
constructor(repository) {
this.repository = repository
}
}
而这里传入的是一段行为。它不是一个长期协作的依赖对象,而是一段会被内部流程调用的逻辑。
所以更朴素地说:这是“创建对象时传入行为”。如果一定要放到设计模式词汇里,它带有构造函数注入的味道,但不用把它讲得太重。
从设计模式角度:控制反转(IoC)
再往上看一层,这段代码体现了 Inversion of Control,也就是控制反转。
普通情况下,外部代码可能会自己决定流程怎么走。但在这种写法里,内部先准备好一些能力:
const done = (value) => {
this.value = value
}
const fail = (reason) => {
this.value = reason
}
然后把这些能力交给外部传入的函数:
executor(done, fail)
控制权在这里发生了交接:
- 内部负责创建能力和维护流程。
- 外部函数负责决定什么时候使用这些能力。
- 内部再根据外部函数的调用结果继续推进自己的状态。
很多异步库、事件系统、路由框架都会这样设计:宿主 API 提供能力和生命周期,业务代码注入具体逻辑。
例如:
setTimeout(() => {
console.log('later')
}, 1000)
button.addEventListener('click', () => {
console.log('clicked')
})
router.get('/users', (req, res) => {
res.send('users')
})
调用方不是自己管理完整流程,而是把逻辑交出去,让宿主在正确的时机调用。
从函数式编程角度:高阶函数(Higher-Order Function)
从函数式编程角度看,这就是高阶函数。
因为:
constructor(executor)
接收了一个函数作为参数。
凡是接收函数作为参数,或者返回函数的函数,都可以叫高阶函数:
function run(callback) {
callback()
}
run(() => {
console.log('running')
})
所以高阶函数强调的是一件事:函数也可以像普通值一样被传递。
这个说法很宽。setTimeout(fn)、array.map(fn)、then(fn) 都可以从这个角度理解。它能说明“传入函数”这件事,但不能说明为什么要把内部能力交出去,也不能说明谁在控制流程。
在 Promise 领域的专业术语
如果把这个模式放到原生 Promise 里,传给构造函数的函数通常叫 executor function。
new Promise((resolve, reject) => {
resolve(123)
})
这里的 (resolve, reject) => {} 就是 executor。
它会在创建实例时立即执行:
new Promise((resolve, reject) => {
console.log('立即执行')
})
// 立即执行
不过这只是这个思路在 Promise 领域里的名字。换到别的 API 里,它可能叫 callback、handler、listener、task、middleware。名字会变,但底层思路很像:内部定义流程,外部提供行为。
这几个名字怎么用
同一段写法,可以按讨论重点选择不同叫法:
- 只说“传进去一个函数”,叫回调函数。
- 强调“函数作为值传递”,叫高阶函数。
- 强调“创建对象时传入一段逻辑”,叫构造函数传入行为。
- 强调“宿主控制流程,调用方提供逻辑”,叫控制反转。
我更愿意先把它理解成一种很朴素的代码组织思路:内部负责搭台,外部负责填内容。
内部 API 不需要提前知道所有业务细节,只要定义好它愿意开放哪些能力、会在什么时候调用外部函数。外部代码也不用接管完整流程,只要把自己的那段行为交进去。
这就是很多 JavaScript API 看起来相似的原因。它们表面上都是“传一个函数进去”,背后其实是在划分职责:谁控制流程,谁补充逻辑。