JavaScript - this、call、apply、bind

#this 关键字概述

与其他语言相比,函数的 this 关键字在 JavaScript 中的表现有着显著差异,在 严格模式非严格模式 下也存在行为上的不同。

在大多数情况下,函数的 调用方式 决定了 this 的值(这被称为 运行时绑定)。this 不能在执行期间被赋值,并且在每次函数被调用时 this 的值可能会发生变化。ES5 引入了 bind 方法来设置函数的 this 值,而不用考虑函数如何被调用。ES2015 引入了 箭头函数,其不提供自身的 this 绑定,而是继承自外部词法作用域的 this 值。

this 的值取决于它出现的上下文:函数上下文类上下文全局上下文

#this 在不同上下文中的指向

#全局上下文

在全局执行上下文中(在任何函数体外部),this 指向全局对象,在浏览器中为 window 对象。

全局上下文中的this
JavaScript
console.log(this) // 在浏览器中输出 window 对象

#函数上下文

在函数内部,this 的值取决于函数的调用方式:

#默认绑定

当函数被直接调用时,this 在非严格模式下指向 全局对象,在严格模式下为 undefined

默认绑定
JavaScript
function fn() {
console.log(this) // 非严格模式下是 window,严格模式下是 undefined
}
fn()
#隐式绑定

当函数作为对象的方法调用时,this 指向调用该方法的对象。

隐式绑定
JavaScript
const weatherData = {
condition: "晴天",
fn() {
console.log(this) // 指向 weatherData 对象
console.log(this.condition) // "晴天"
},
}
weatherData.fn()

但要注意,如果将该方法赋值给一个变量后再调用,this 会丢失原来的指向:

隐式绑定丢失
JavaScript
const weatherData = {
condition: "多云",
fn() {
console.log(this.condition)
},
}
const getFn = weatherData.fn // 函数引用赋值给变量
getFn() // 输出 undefined,因为此时 this 指向全局对象
#显式绑定

通过 callapplybind 方法,我们可以显式地指定函数执行时的 this 值。

显式绑定
JavaScript
function reportWeather() {
console.log(`今天的天气是:${this.condition}`)
}
const forecast = { condition: "雨天" }
reportWeather.call(forecast) // 输出:今天的天气是:雨天
#构造函数绑定

当使用 new 操作符调用函数时,函数内部的 this 会指向新创建的对象实例。

构造函数绑定
JavaScript
function Weather(condition) {
this.condition = condition
console.log(this) // 指向新创建的 Weather 实例
}
const weather = new Weather("大风")
console.log(weather.condition) // "大风"
#箭头函数中的 this

箭头函数 不会创建自己的 this 上下文,而是继承父级作用域的 this 值。这意味着箭头函数中的 this 与定义它的上下文中的 this 相同。

箭头函数中的this
JavaScript
const forecast = {
condition: "雪天",
regularFunc: function () {
console.log(this.condition) // "雪天"
const arrowFunc = () => {
console.log(this.condition) // 仍然是 "雪天",因为继承了 regularFunc 中的 this
}
arrowFunc()
},
}
forecast.regularFunc()

#改变 this 指向的方法

JavaScript 提供了三种主要方法来改变函数执行时的 this 指向:callapplybind

#call

call 方法允许调用一个函数,并明确指定函数执行时的 this 值,同时可以传入一系列参数。

#call 语法
JavaScript
function.call(thisArg, arg1, arg2, ...)

thisArg

调用 func 时提供的 this 值。如果函数不处于严格模式,则 nullundefined 会被替换为全局对象,原始值会被转换为对象。

argsArray 可选

函数的参数。

返回值

使用指定的 this 值和参数调用函数的结果。

#call 示例用法
call方法示例
JavaScript
function forecast(time1, time2) {
console.log(`${time1}${time2}的天气是${this.condition}`)
}
const weather = { condition: "晴朗" }
forecast.call(weather, "上午", "下午") // 输出:上午和下午的天气是晴朗
#手动实现 call 方法
手动实现call方法
JavaScript
Function.prototype.myCall = function (context, ...args) {
// 处理 context 为 null 或 undefined 的情况
context = context == null ? globalThis : Object(context)
// 使用 Symbol 创建唯一的方法名,避免属性名冲突
const uniqueKey = Symbol("tempMethod")
// 将当前函数设为 context 的一个方法
context[uniqueKey] = this
// 调用该方法
const result = context[uniqueKey](...args)
// 删除添加的临时方法
delete context[uniqueKey]
// 返回函数执行结果
return result
}

#apply

Function 实例的 apply() 方法会以给定的 this 值和作为数组(或类数组对象)提供的 arguments 调用该函数。

#apply 语法
JavaScript
function.apply(thisArg, [argsArray])

thisArg

调用 func 时提供的 this 值。如果函数不处于严格模式,则 nullundefined 会被替换为全局对象,原始值会被转换为对象。

argsArray 可选

一个类数组对象,用于指定调用 func 时的参数,或者如果不需要向函数提供参数,则为 nullundefined

返回值

使用指定的 this 值和参数调用函数的结果。

#apply 示例用法
apply方法示例
JavaScript
function forecast(time1, time2) {
console.log(`${time1}${time2}的天气是${this.condition}`)
}
const weather = { condition: "多云转阴" }
forecast.apply(weather, ["早晨", "傍晚"]) // 输出:早晨和傍晚的天气是多云转阴
#手动实现 apply 方法
手动实现apply方法
JavaScript
Function.prototype.myApply = function (context, argsArray = []) {
// 处理 context 为 null 或 undefined 的情况
context = context == null ? globalThis : Object(context)
// 使用 Symbol 创建唯一的方法名,避免属性名冲突
const uniqueKey = Symbol("tempMethod")
// 将当前函数设为 context 的一个方法
context[uniqueKey] = this
// 调用该方法,传入参数数组
const result = context[uniqueKey](...argsArray)
// 删除添加的临时方法
delete context[uniqueKey]
// 返回函数执行结果
return result
}

#bind

bind 方法与 callapply 不同,它不会立即调用函数,而是返回一个新函数,新函数的 this 值被永久地绑定到指定的值。

#bind 语法
JavaScript
const newFunc = function.bind(thisArg, arg1, arg2, ...)

thisArg

调用 func 时提供的 this 值。如果函数不处于严格模式,则 nullundefined 会被替换为全局对象,原始值会被转换为对象,并且原始值会被转换为对象。如果使用 new 运算符构造绑定函数,则忽略该值。

argsArray 可选

在调用 func 时,插入到传入绑定函数的参数前的参数。

返回值

使用指定的 this 值和初始参数(如果提供)创建的给定函数的副本。

#bind 示例用法
bind方法示例
JavaScript
function forecast(period1, period2) {
console.log(`${period1}${period2}的天气是${this.condition}`)
}
const weather = { condition: "雷阵雨" }
const morningForecast = forecast.bind(weather, "早上")
morningForecast("中午") // 输出:早上和中午的天气是雷阵雨
#手动实现 bind 方法
手动实现bind方法
JavaScript
Function.prototype.myBind = function (context, ...initialArgs) {
// 保存原函数引用
const originalFunc = this
// 返回一个新函数
return function boundFunc(...args) {
// 处理构造函数调用的情况(使用 new 操作符)
if (this instanceof boundFunc) {
// 如果是通过 new 调用,则忽略 context,将 this 指向新创建的对象
return new originalFunc(...initialArgs, ...args)
}
// 否则,使用 apply 方法调用原函数,绑定 this 和合并参数
return originalFunc.apply(context, [...initialArgs, ...args])
}
}

#this 绑定的优先级

当多种 this 绑定规则同时适用时,它们遵循一定的优先级:

  1. 构造函数绑定(new):最高优先级
  2. 显式绑定(call、apply、bind):次高优先级
  3. 隐式绑定(对象方法调用):第三优先级
  4. 默认绑定(独立函数调用):最低优先级

#常见陷阱和最佳实践

#嵌套函数中的 this

在嵌套函数中,内部函数不会自动继承外部函数的 this 值:

嵌套函数中的this
JavaScript
const forecast = {
condition: "阴天",
outerMethod() {
console.log(this.condition) // "阴天"
function innerMethod() {
console.log(this.condition) // undefined(在非严格模式下,指向全局对象)
}
innerMethod()
},
}
forecast.outerMethod()

解决方案:

  1. 使用 箭头函数,它会继承父作用域的 this
  2. 在外部函数中保存 this 引用(如 const self = this
  3. 使用 bind 方法明确绑定内部函数的 this

#回调函数中的 this

在将方法作为回调函数传递时,常常会丢失 this 上下文:

回调函数中的this
JavaScript
const temperature = {
value: "26°C",
display() {
console.log(`当前温度:${this.value}`)
},
}
// this 指向丢失
setTimeout(temperature.display, 1000) // 输出:当前温度:undefined

解决方案:

  1. 使用 bind 方法绑定正确的 this:setTimeout(temperature.display.bind(temperature), 1000)
  2. 使用 箭头函数 封装调用:setTimeout(() => temperature.display(), 1000)

#总结

理解 JavaScript 中的 this 机制是编写高质量代码的关键。通过掌握不同上下文中 this 的行为规则,以及 callapplybind 方法的使用,可以更精确地控制函数执行的上下文,避免常见的 this 指向错误,提高代码的可维护性和健壮性。