JavaScript 的 call、apply 和 bind:如何控制函数调用

JavaScript 的 call、apply 和 bind:如何控制函数调用

JavaScript 中,一个普通函数可以配合不同对象使用,也可以预先固定参数,交给其他代码稍后调用。call、apply 和 bind 提供了这些能力。

要理解它们,先从 this 与调用方式的关系看起。

一、this 为什么会随着调用方式改变?

看下面这个对象:

javascript 复制代码
const user = {
  name: '小明',

  sayName() {
    return this.name
  }
}

console.log(user.sayName()) // 小明

调用表达式是 user.sayName(),因此这次调用中,sayName 内部的 this 指向 user。

但如果把方法取出来,再单独调用,结果就不同了:

javascript 复制代码
'use strict'

const user = {
  name: '小明',

  sayName() {
    return this.name
  }
}

const sayName = user.sayName

sayName() // TypeError:this 是 undefined,无法读取 name

const sayName = user.sayName 只是把函数赋给另一个变量,没有同时绑定 user。严格模式下,普通函数直接调用时,this 是 undefined。

因此,函数来自哪个对象,并不能永久决定它的 this;调用方式才是关键。

如果希望调用时明确指定 this,就可以使用 call。

二、call:指定 this,并立即执行

call 的基本形式是:

javascript 复制代码
fn.call(thisArg, arg1, arg2)

第一个参数指定函数内部的 this,后面的参数按顺序传给原函数。

例如:

javascript 复制代码
function introduce(greeting, punctuation) {
  return greeting + ',我是' + this.name + punctuation
}

const user = { name: '小明' }

console.log(introduce.call(user, '你好', '!'))
// 你好,我是小明!

这次调用中:

  • user 成为函数内部的 this。
  • '你好' 传给 greeting。
  • '!' 传给 punctuation。

user 不会占用函数的第一个参数位置,它是提供给 call 的调用上下文。

换一个对象,同一段函数逻辑就能生成不同结果:

javascript 复制代码
introduce.call({ name: '小红' }, '早上好', '。')
// 早上好,我是小红。

这正是 call 的用途:复用已有函数,同时明确指定这次调用使用的对象。

三、apply:把数组中的元素作为参数传入

apply 同样可以指定 this 并立即执行函数,只是参数的组织方式不同:

javascript 复制代码
fn.apply(thisArg, args)

当参数已经保存在数组中时,可以直接交给 apply:

javascript 复制代码
const user = { name: '小明' }
const args = ['你好', '!']

console.log(introduce.apply(user, args))
// 你好,我是小明!

apply 会把数组元素按顺序作为实参传入:

javascript 复制代码
args[0] → greeting
args[1] → punctuation

因此,下面两种写法表达了相同的调用:

javascript 复制代码
introduce.call(user, '你好', '!')

introduce.apply(user, ['你好', '!'])

选择哪一种,主要看调用者手中的参数是分散的值,还是已经集中在数组中。

展开语法与 apply

现代 JavaScript 可以使用展开语法,把数组展开为多个实参:

javascript 复制代码
const args = ['你好', '!']

introduce.call(user, ...args)

对于普通数组,这能表达与 apply 相同的效果。

如果函数不需要指定 this,写法可以更直接:

javascript 复制代码
const numbers = [10, 20, 30]

Math.max(...numbers) // 30

对应的 apply 写法是:

javascript 复制代码
Math.max.apply(null, numbers) // 30

展开语法覆盖了 apply 的许多常见用途,不过旧项目中仍然经常能看到 apply。

两者还有一个细节差异:apply 可以接收类数组对象,而展开语法要求被展开的值可迭代。普通数组同时满足这两个条件,因此一般数组示例看不出区别。

四、bind:先固定调用条件,返回一个新函数

call 和 apply 都会立即执行函数,bind 则返回一个新函数:

javascript 复制代码
const boundFn = fn.bind(thisArg, arg1)

新函数会记住指定的 this,以及预先提供的参数。

继续使用 introduce:

javascript 复制代码
const user = { name: '小明' }

const sayHello = introduce.bind(user, '你好')

执行这一行时,introduce 尚未运行。得到的 sayHello 已经固定了两个条件:

  • this 使用 user。
  • 第一个参数使用 '你好'。

调用时只需要提供剩下的参数:

javascript 复制代码
sayHello('!') // 你好,我是小明!
sayHello('。') // 你好,我是小明。

这种先固定部分参数、再得到新函数的方式,叫作部分应用(partial application)。

即使函数不使用 this,也可以利用 bind 固定参数:

javascript 复制代码
function multiply(a, b) {
  return a * b
}

const double = multiply.bind(null, 2)

double(5)  // 10
double(10) // 20

double 固定了第一个参数 2,调用者只需提供第二个参数。

这里传入 null,是因为 multiply 没有使用 this,所以这个值不会影响计算结果。

bind 后面的括号表示什么?

看下面这行代码:

javascript 复制代码
multiply.bind(null, 2, 5)() // 10

它实际上包含两步:

javascript 复制代码
const boundMultiply = multiply.bind(null, 2, 5)

const result = boundMultiply()

bind(...) 创建新函数,后面的 () 才执行它。如果没有最后这对括号,得到的只是函数,还没有计算结果。

五、为什么回调函数经常需要 bind?

对象方法作为回调传出去后,后续调用未必会以原对象作为 this。

例如:

javascript 复制代码
const user = {
  name: '小明',

  sayName() {
    console.log(this.name)
  }
}

setTimeout(user.sayName.bind(user), 100)
// 稍后输出:小明

这里先通过 bind(user) 创建一个绑定了 user 的函数,再交给 setTimeout 稍后执行。

需要注意,固定 this 不等于复制对象数据。新函数记住的是对象引用,执行时仍会读取对象当时的属性:

javascript 复制代码
const sayName = user.sayName.bind(user)

user.name = '小红'

sayName() // 小红

回调也可以通过箭头函数包装:

javascript 复制代码
setTimeout(() => user.sayName(), 100)

稍后执行的是 user.sayName(),所以方法内部的 this 同样指向 user。

六、箭头函数的 this 不能重新绑定

前面的绑定示例使用的都是普通函数。

箭头函数的 this 来自定义时所在的外层作用域,不能通过 call、apply 或 bind 重新指定。

javascript 复制代码
function createReader() {
  return () => this.name
}

const reader = createReader.call({ name: '小明' })

reader.call({ name: '小红' }) // 小明

创建 reader 时,箭头函数捕获了外层函数中的 this。之后使用 call,也无法把它改成"小红"。

不过,bind 仍然可以固定箭头函数的参数:

javascript 复制代码
const add = (a, b) => a + b

const addTen = add.bind(null, 10)

addTen(20) // 30

也就是说,绑定 this 与固定参数是两个独立作用 。箭头函数不接受新的 this,但仍然可以接收预先绑定的参数。

七、TypeScript 如何检查这些调用?

JavaScript 提供调用机制,TypeScript 则可以检查调用上下文和参数是否符合类型要求。

例如,显式声明 introduce 所需的 this 类型:

javascript 复制代码
function introduce(
  this: { name: string },
  greeting: string,
  punctuation: string
): string {
  return greeting + ',我是' + this.name + punctuation
}

这里的 this 参数只用于类型检查,不是运行时的普通形参,也不会占用 greeting 的位置。

启用严格检查后:

javascript 复制代码
introduce.call({ name: '小明' }, '你好', '!') // 正确

introduce.call({ name: 123 }, '你好', '!')
// 类型错误:name 应该是 string

使用 apply 时,可以用元组准确表达参数的位置和类型:

javascript 复制代码
const args: [string, string] = ['你好', '!']

introduce.apply({ name: '小明' }, args)

[string, string] 表示两个位置都必须是字符串,与函数的两个普通参数对应。

这些调用的类型检查由 strictBindCallApply 控制,启用 strict 时也会启用它。类型检查帮助我们提前发现错误,JavaScript 的运行时调用规则仍然照常生效。

八、实际使用时如何选择?

场景 写法
普通函数调用 fn(a, b)
指定 this,逐个提供参数 fn.call(obj, a, b)
指定 this,参数保存在数组或类数组中 fn.apply(obj, args)
固定 this 或部分参数,得到供后续调用的新函数 fn.bind(obj, a)

写代码时可以先判断:这次是否需要指定 this?参数是否已经集中在数组里?需要的是执行结果,还是一个可交给其他代码的函数?

这些需求确定后,对应的调用方式也就清楚了。

相关推荐
weixin_307779131 小时前
C++代码实现MATLAB中的cvpartition函数功能
开发语言·c++·算法·matlab
黑妹天下第一乖1 小时前
小智改造实战解读-首 token 延迟去哪了:云端与端侧大模型的分段对照
开发语言·人工智能·python·嵌入式硬件·自然语言处理·iot
。小二1 小时前
Go 泛型并发利刃:async 库深度评测——370K ops/s、零依赖、生产就绪
开发语言·javascript·golang
CopyCode1 小时前
Agent 开发不是模型训练:一个前端的入门认知
前端·llm·agent
(Charon)1 小时前
【C++面试】线程同步机制:互斥锁、条件变量、原子操作与读写锁
开发语言·c++·算法·面试
吠品1 小时前
HTTPS 真身:HTTP 套了层 TLS
java·服务器·前端
Highcharts.js1 小时前
Highcharts动态图表与数据实时更新应用讲解
javascript·highcharts·前端可视化·动态图表·交互图表·图表教程·实时刷新
JYeontu1 小时前
实现一个「曲线滑块验证」功能
前端·javascript·html
Csvn1 小时前
组件库(从 API 契约到 Headless 内核)
前端