📍 this
JavaScript 中的 this
关键字用于指代当前执行上下文,但由于 JavaScript 语言的动态特性,this
的值也是动态的(运行时绑定),并且在不同的场景下可能会有不同的值。
🌎 全局上下文
在全局上下文中,this
值为全局对象,全局对象取决于运行环境,如下。
运行环境 | 全局对象 |
---|---|
浏览器(Browser) | window |
Node.js | global |
tip:
不建议在全局上下文中使用
this
,而应使用window
或global
,以避免混淆和潜在的错误。ES11引入了新的全局对象
globalThis
,它提供了一种跨平台访问全局环境的标准方式。
📦 模块上下文
在模块上下文中,this
的值便不再是全局对象。同时,不同的运行环境,模块上下文中的this
值也是不同的,如下。
运行环境 | this值 |
---|---|
浏览器(Browser) | undefined |
Node.js | module.exports |
tip:
- 当
module.exports
重新赋值时,this
与module.exports
便不相等了。- 不建议在模块上下文中使用
this
,而应使用Node模块中提供的exports
或module.exports
。
🧩 函数上下文
在函数上下文中, this
值在定义函数时是无法确定的,而是根据运行时函数被调用的方式来确定,以下是几种常见情况。
调用方式 | 示例 | 函数中的this值 |
---|---|---|
全局中直接调用 | method() | 全局对象 |
模块中直接调用 | method() | undefined /module.exports |
通过new 关键字调用 | new method() | 新对象 |
通过对象的属性调用 | obj.method() | 所属对象 |
通过call 方法调用 | method.call(context) | 第一个参数,即context |
通过apply 方法调用 | method.apply(context) | 第一个参数,即context |
通过bind 方法生成新函数,再调用新函数 | method.bind(context)() | bind 方法的第一个参数,即context |
需要注意的是,调用全局对象中的函数时,尽管全局对象可以省略,但此时函数体中的this
值依然为函数所属对象。例如下面的函数中的this
值依然为window
,运行环境为浏览器。
此外,箭头函数没有自身的this
绑定,出现在箭头函数内部中的this
值与其定义时最近的外层上下文中的this
值保持一致。以下为箭头函数this
值示例,运行环境为浏览器。
同时,由于箭头函数没有this
指针,通过apply
、call
或bind
方法调用一个函数时,第一个参数将会被忽略。
✍️ call、apply、bind
call
Function.prototype.call
方法用于显式地调用一个函数(API调用),并在调用时可以指定函数的this
值和参数列表。
call
方法是Function
原型上的方法,常用的语法是通过method.call(context)
进行调用。在调用过程中,call
函数体内部的this
值会被设置为method
。由此,我们可以直接在call
函数体内部间接调用context.method()
,并将参数传递进去。这样一来,method
函数调用时其内部的this
就指向了传入的context
对象。
从上图可知,实现call
方法的重点是将method
作为context
身上的属性,并通过context.method()
调用即可,method
的值便是call
函数体中的this
值,但此过程仍需注意以下几点。
- 若传递的
context
值为null
或undefined
,则this
值为全局对象。 - 若传递的
context
值为基本数据类型,如string、number、boolean等,context
将被包装。 - 当
method
作为context
对象的属性时,其属性名必须唯一,且需要在call
方法结束后删除。
Function.prototype.call = function (context, ...args) {
context = context === null || context === undefined
? globalThis : Object(context)
const tempKey = Symbol('temp')
Object.defineProperty(context, tempKey, {
value: this,
enumerable: false
})
const result = context[tempKey](...args)
delete context[tempKey]
return result
}
apply
Function.prototype.apply
方法与call
方法类似,也用于显式地调用一个函数(API调用),并在调用时可以指定函数的this
值和参数列表。
与call
方法不同的是,apply
方法接收的参数列表不是展开的,而是数组。所以,我们只需要将数组展开,调用call
方法即可,但此过程仍需注意以下几点。
- 当参数列表,即方法的第二个参数为
null
或undefined
时,原函数调用时无参数。 - 当参数列表,即方法的第二个参数不是数组类型,且不是
null
或undefined
,则抛出错误。 apply
函数体内的this
值为原函数。
Function.prototype.apply = function (context, args) {
this.call(context, ...(args || []))
}
bind
Function.prototype.bind
方法用于创建一个新的绑定函数,新函数的this
值为bind
方法的第一个参数。bind
方法还可以接受额外的参数,这些参数会被提前绑定到新函数的参数列表。当调用新函数时,这些绑定的参数和传入的参数会一起传递给原函数。
与call
、apply
方法不同的是,bind
函数的调用不会触发原函数的执行,而是返回新的绑定函数。我们可以在绑定函数中调用apply
来触发原函数的执行,并在调用时合并参数列表,但此过程仍需注意以下几点。
- 绑定函数也可以被当作构造函数使用,此时
bind
方法绑定的this
值将被忽略,函数体中的this
值将指向新实例,合并的参数列表依然传入。 bind
函数体内的this
值为原函数。
Function.prototype.bind = function (context, ...args) {
const func = this
const boundFunc = function (...argArray) {
if (this instanceof boundFunc) {
return new func(...args, ...argArray)
}
return func.apply(context, argArray.concat(args))
}
Object.setPrototypeOf(func.prototype, boundFunc.prototype)
return boundFunc
}