← 返回文章列表

函数传参与控制反转

前端
  • # 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 看起来相似的原因。它们表面上都是“传一个函数进去”,背后其实是在划分职责:谁控制流程,谁补充逻辑。