JavaScript 中 call、apply、bind 的区别与实现
问题解析
call、apply、bind 都是 JavaScript 中用于改变函数执行时 this 指向的方法,但它们在用法和返回值上有重要区别。
核心概念
三者的共同点
- 改变
this指向:都能改变函数执行时的上下文(this指向) - 继承自
Function.prototype:所有函数都可以调用这些方法 - 第一个参数都是
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:call 和 apply 的区别是什么?什么时候用哪个?
- 两者都立即执行函数并改变
this指向 call参数逐个传递,apply参数以数组形式传递- 参数确定时用
call,参数以数组形式存在时用apply(ES6 后可用call+ 展开运算符替代)
Q:bind 和 call/apply 的最大区别是什么?
bind返回一个新函数,不会立即执行bind支持柯里化,可以分多次传入参数bind绑定是永久的,返回的函数this无法改变(除了用new调用)
Q:如何手写一个 call 方法?
核心步骤:
- 将函数挂载到
thisArg对象上 - 通过该对象调用函数(此时
this自然指向该对象) - 删除临时挂载的函数
- 返回执行结果
Q:为什么 bind 返回的函数用 new 调用时,this 会指向实例而不是绑定的对象?
这是 JavaScript 的规范要求。当 bind 返回的函数作为构造函数使用时,绑定的 thisArg 被忽略,this 指向新创建的实例。这保证了继承链的正确性。