返回首页

JavaScript 中 call、apply、bind 的区别与实现

问题解析

callapplybind 都是 JavaScript 中用于改变函数执行时 this 指向的方法,但它们在用法和返回值上有重要区别。


核心概念

三者的共同点

  1. 改变 this 指向:都能改变函数执行时的上下文(this 指向)
  2. 继承自 Function.prototype:所有函数都可以调用这些方法
  3. 第一个参数都是 thisArg:用于指定函数执行时的 this

三者的区别

特性 call apply bind
调用时机 立即执行 立即执行 返回新函数,不立即执行
参数传递 逐个传递 数组传递 逐个传递(柯里化支持)
返回值 函数执行结果 函数执行结果 绑定 this 后的新函数
使用场景 知道参数个数时 参数是数组/类数组时 需要延迟执行或柯里化时

基本用法

call 方法

function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`)
}

const person = { name: 'Alice' }

// 第一个参数是 this 指向,后面参数逐个传递
greet.call(person, 'Hello', '!')  // "Hello, Alice!"

apply 方法

function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`)
}

const person = { name: 'Bob' }

// 第一个参数是 this 指向,第二个参数是数组
const args = ['Hi', '?']
greet.apply(person, args)  // "Hi, Bob?"

bind 方法

function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`)
}

const person = { name: 'Charlie' }

// 返回一个新函数,this 被永久绑定
const greetCharlie = greet.bind(person)
greetCharlie('Hey', '.')  // "Hey, Charlie."

// bind 支持柯里化(预设参数)
const sayHelloToCharlie = greet.bind(person, 'Hello')
sayHelloToCharlie('!')  // "Hello, Charlie!"

使用场景对比

场景 1:借用方法

// 借用数组方法操作类数组
function sum() {
  // arguments 是类数组,没有 forEach 方法
  const args = Array.prototype.slice.call(arguments)
  // 或: const args = Array.from(arguments)

  return args.reduce((a, b) => a + b, 0)
}

sum(1, 2, 3, 4)  // 10

场景 2:数组方法的最大值

const numbers = [5, 2, 8, 1, 9]

// apply 适合参数是数组的场景
const max = Math.max.apply(null, numbers)
// ES6 之后更推荐: Math.max(...numbers)

console.log(max)  // 9

场景 3:回调函数保持 this

const user = {
  name: 'David',
  sayHi: function() {
    console.log(`Hi, I'm ${this.name}`)
  },
  sayHiLater: function() {
    // 使用 bind 确保回调中的 this 指向正确
    setTimeout(this.sayHi.bind(this), 1000)

    // 或者用箭头函数(ES6)
    // setTimeout(() => this.sayHi(), 1000)
  }
}

user.sayHiLater()  // 1秒后: "Hi, I'm David"

场景 4:函数柯里化

function multiply(a, b, c) {
  return a * b * c
}

// bind 的柯里化特性
const multiplyByTwo = multiply.bind(null, 2)
const multiplyByTwoAndThree = multiplyByTwo.bind(null, 3)

multiplyByTwoAndThree(4)  // 2 * 3 * 4 = 24

手写实现

手写 call

Function.prototype.myCall = function(thisArg, ...args) {
  // 1. 判断调用者是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('myCall must be called on a function')
  }

  // 2. 处理 thisArg 为 null 或 undefined 的情况
  thisArg = thisArg ?? globalThis  // ES2020 语法,或: thisArg == null ? window : Object(thisArg)

  // 3. 将 thisArg 转为对象
  const obj = Object(thisArg)

  // 4. 创建一个唯一属性名,避免覆盖原有属性
  const fnKey = Symbol('fn')

  // 5. 将当前函数绑定到 thisArg 上
  obj[fnKey] = this

  // 6. 执行函数并获取结果
  const result = obj[fnKey](...args)

  // 7. 删除临时属性
  delete obj[fnKey]

  // 8. 返回结果
  return result
}

// 测试
function greet(greeting) {
  console.log(`${greeting}, ${this.name}!`)
}

const person = { name: 'Alice' }
greet.myCall(person, 'Hello')  // "Hello, Alice!"

手写 apply

Function.prototype.myApply = function(thisArg, argsArray) {
  // 1. 判断调用者是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('myApply must be called on a function')
  }

  // 2. 处理 argsArray 参数(可选)
  if (argsArray != null && !Array.isArray(argsArray) &&
      typeof argsArray[Symbol.iterator] !== 'function') {
    throw new TypeError('CreateListFromArrayLike called on non-object')
  }

  // 3. 处理 thisArg
  thisArg = thisArg ?? globalThis
  const obj = Object(thisArg)

  // 4. 创建唯一属性名
  const fnKey = Symbol('fn')
  obj[fnKey] = this

  // 5. 执行函数(处理无参数情况)
  let result
  if (argsArray == null) {
    result = obj[fnKey]()
  } else {
    result = obj[fnKey](...argsArray)
  }

  // 6. 清理并返回
  delete obj[fnKey]
  return result
}

// 测试
function sum(a, b, c) {
  return a + b + c
}

sum.myApply(null, [1, 2, 3])  // 6
sum.myApply(null)             // NaN (a,b,c 都是 undefined)

手写 bind

Function.prototype.myBind = function(thisArg, ...presetArgs) {
  // 1. 判断调用者是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('myBind must be called on a function')
  }

  // 2. 保存原始函数的引用
  const originalFn = this

  // 3. 返回一个绑定函数
  function boundFn(...laterArgs) {
    // 4. 如果是通过 new 调用,this 应该指向实例
    const isNew = this instanceof boundFn
    const context = isNew ? this : thisArg

    // 5. 合并预设参数和后续参数
    const finalArgs = [...presetArgs, ...laterArgs]

    // 6. 调用原始函数
    return originalFn.apply(context, finalArgs)
  }

  // 7. 维护原型链(如果原始函数有 prototype)
  if (originalFn.prototype) {
    boundFn.prototype = Object.create(originalFn.prototype)
  }

  // 8. 可选:设置 boundFn 的 length 和 name 属性
  Object.defineProperty(boundFn, 'length', {
    value: Math.max(0, originalFn.length - presetArgs.length),
    writable: false
  })

  return boundFn
}

// 测试基础用法
function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`)
}

const person = { name: 'Bob' }
const greetBob = greet.myBind(person, 'Hello')
greetBob('!')  // "Hello, Bob!"

// 测试 new 调用
function Person(name, age) {
  this.name = name
  this.age = age
}

const CreatePerson = Person.myBind(null, 'Alice')
const alice = new CreatePerson(25)
console.log(alice)  // Person { name: 'Alice', age: 25 }

完整版 bind(包含所有边界情况)

Function.prototype.myBindComplete = function(thisArg, ...presetArgs) {
  if (typeof this !== 'function') {
    throw new TypeError(this + ' is not a function')
  }

  const originalFn = this

  // 创建空函数用于原型链中转
  const EmptyFn = function() {}

  function boundFn(...laterArgs) {
    // 当作为构造函数调用时,this 指向实例;否则指向绑定的 thisArg
    const isConstructing = this instanceof boundFn
    const context = isConstructing ? this : thisArg

    return originalFn.apply(context, [...presetArgs, ...laterArgs])
  }

  // 优化原型链设置
  if (originalFn.prototype) {
    EmptyFn.prototype = originalFn.prototype
    boundFn.prototype = new EmptyFn()
    EmptyFn.prototype = null  // 防止内存泄漏
  }

  // 设置 constructor
  boundFn.prototype.constructor = boundFn

  // 设置 toString
  boundFn.toString = function() {
    return originalFn.toString()
  }

  return boundFn
}

关键要点总结

1. thisArg 的处理规则

function test() {
  console.log(this)
}

// 非严格模式下:
test.call(null)      // window / global
test.call(undefined) // window / global
test.call(123)       // Number(123) 对象
test.call('abc')     // String('abc') 对象
test.call(true)      // Boolean(true) 对象

// 严格模式下:
'use strict'
test.call(null)      // null
test.call(123)       // 123(原始值,不装箱)

2. 使用 Symbol 的原因

手写实现中使用 Symbol 作为临时属性名,是为了避免覆盖 thisArg 上的同名属性。在 ES6 之前,通常使用随机字符串或时间戳来模拟唯一键。

3. bind 的特殊性

  • 柯里化bind 可以预设部分参数,返回的函数可以接收剩余参数
  • 构造函数处理:当 bind 返回的函数作为构造函数使用时,this 应该指向新创建的实例,而不是绑定的 thisArg
  • 原型链:需要正确处理原型链,确保 instanceof 判断正确

常见面试题

**Q:callapply 的区别是什么?什么时候用哪个?

  • 两者都立即执行函数并改变 this 指向
  • call 参数逐个传递,apply 参数以数组形式传递
  • 参数确定时用 call,参数以数组形式存在时用 apply(ES6 后可用 call + 展开运算符替代)

Q:bindcall/apply 的最大区别是什么?

  • bind 返回一个新函数,不会立即执行
  • bind 支持柯里化,可以分多次传入参数
  • bind 绑定是永久的,返回的函数 this 无法改变(除了用 new 调用)

Q:如何手写一个 call 方法?

核心步骤:

  1. 将函数挂载到 thisArg 对象上
  2. 通过该对象调用函数(此时 this 自然指向该对象)
  3. 删除临时挂载的函数
  4. 返回执行结果

Q:为什么 bind 返回的函数用 new 调用时,this 会指向实例而不是绑定的对象?

这是 JavaScript 的规范要求。当 bind 返回的函数作为构造函数使用时,绑定的 thisArg 被忽略,this 指向新创建的实例。这保证了继承链的正确性。