JS 的 this 指向
Category(分类): JavaScript Status: 已整理(2026)
this的值不是由函数定义位置单独决定的。对普通函数来说,最重要的是调用形式;对箭头函数来说,this来自定义时的词法环境;class、DOM 事件、模块和 Node.js 还会叠加宿主语境。本文保留原文的调用模式、call/apply/bind、new、箭头函数、事件处理函数和优先级主线,并修正全局、定时器和旧浏览器 API 的绝对化说法。
一、先记住一张规则表
| 调用或定义方式 | 普通函数中的 this |
|---|---|
new Fn() | 新创建的实例;构造器返回对象时可能被替换 |
fn.call(value, ...) / fn.apply(value, ...) | 严格模式使用 value;非严格模式会发生 this substitution |
obj.fn() | obj,也就是调用表达式左侧的引用基值 |
fn() | 严格模式为 undefined;非严格模式通常为 globalThis |
| 箭头函数 | 不创建自己的 this,捕获外层词法 this |
| class 方法 | 和普通方法一样取决于调用者,但 class 默认严格模式 |
| DOM 普通事件监听器 | 通常是 event.currentTarget |
这张表不是按照代码缩进判断,而是按照函数真正被调用的表达式判断:
'use strict'
function getName() {
return this?.name
}
const first = { name: 'first', getName }
const second = { name: 'second', getName }
console.log(first.getName()) // first
console.log(second.getName()) // second
const detached = first.getName
console.log(detached()) // undefined:严格模式下是 undefined
同一个函数被不同对象调用时,this 可以不同。函数“被定义在哪里”与函数“如何被调用”是两个问题。
二、全局上下文中的 this
原文说“严格模式和非严格模式中全局 this 都是 window”,需要补充运行方式:
| 运行方式 | 顶层 this 的典型值 |
|---|---|
浏览器经典 <script> | window,也就是 globalThis |
浏览器 <script type="module"> | undefined |
| Node.js CommonJS 文件 | module.exports(Node 的模块包装语境) |
| Node.js ESM 文件 | undefined |
因此跨环境代码应优先使用 globalThis,不要假设一定存在 window:
console.log(globalThis)
if (typeof window !== 'undefined') {
console.log('当前环境提供 window')
}
经典浏览器脚本中的顶层 var 还会成为全局对象属性,而 let、const 和 class 不会:
<script>
var varValue = 'var value'
let letValue = 'let value'
console.log(window.varValue) // var value
console.log(window.letValue) // undefined
</script>
这是全局环境记录的属性绑定规则,不是 this 的通用规律。模块顶层默认严格模式,并且模块顶层 this 是 undefined。
三、普通函数调用:默认绑定
3.1 非严格模式
在非严格脚本中,普通函数直接调用时,如果没有显式的调用者,this substitution 会把 undefined/null 替换为 globalThis,原始值则包装成对应的对象:
// 这段示例应放在非严格脚本中运行
function nonStrictThis() {
return this
}
console.log(nonStrictThis() === globalThis) // true(非严格脚本)
console.log(nonStrictThis.call(null) === globalThis) // true
console.log(Object.prototype.toString.call(nonStrictThis.call(1)))
// [object Number]
不要把“非严格模式的普通函数 this 一定是 window”扩展到所有宿主:浏览器的全局对象是 window,Node.js 等环境应使用 globalThis 这一标准名称。
3.2 严格模式
严格模式不会做 this substitution:
function strictThis() {
'use strict'
return this
}
console.log(strictThis()) // undefined
console.log(strictThis.call(null)) // null
console.log(strictThis.call(1)) // 1:原始值不会被包装
原文中直接访问严格模式普通函数里的 this.name 会抛出 TypeError,因为 this 是 undefined:
function readName() {
'use strict'
return this.name
}
try {
readName()
} catch (error) {
console.log(error.name) // TypeError
}
ES modules、class 的方法和 class 构造器默认处于严格模式。
3.3 回调函数不是一种特殊的 this 绑定
把函数作为参数传给另一个函数后,this 由调用这个回调的 API决定。数组迭代方法通常以未绑定的方式调用回调;如果需要回调中的 this,使用 API 提供的 thisArg 或显式绑定:
function logThis() {
'use strict'
return this
}
console.log([1].map(logThis)[0]) // undefined
console.log([1].map(logThis, { name: 'context' })[0])
// { name: 'context' }
不要简单地把所有回调都类比为 fn.call(undefined);定时器、DOM 事件、Node.js EventEmitter 等宿主/API 可能规定不同的回调接收者。最稳妥的方式是使用箭头函数捕获外层 this,或使用 bind:
class Counter {
value = 0
start() {
setTimeout(() => {
this.value += 1
}, 0)
}
}
在浏览器中,经典 setTimeout 的普通回调通常以 window 为 this;Node.js 定时器的回调接收者实现细节不同。业务代码不应依赖这种差异。
四、对象方法调用:隐式绑定
当调用形式是 object.method() 时,普通函数中的 this 是调用表达式左侧的对象:
const person = {
name: 'Ada',
sayName() {
return this.name
},
child: {
name: 'Grace',
sayName() {
return this.name
}
}
}
console.log(person.sayName()) // Ada
console.log(person.child.sayName()) // Grace
真正重要的是“谁在调用”,而不是方法最初被定义在哪个对象上:
function sayName() {
'use strict'
return this.name
}
const first = { name: 'first', sayName }
const second = { name: 'second', sayName }
console.log(first.sayName()) // first
console.log(second.sayName()) // second
4.1 把方法取出来会改变调用形式
const student = {
name: 'student',
sayName() {
'use strict'
return this.name
}
}
const studentSayName = student.sayName
try {
studentSayName()
} catch (error) {
console.log(error.name) // TypeError:严格模式裸调用的 this 是 undefined
}
console.log(studentSayName.call(student)) // student
赋值、解构、传递回调都可能让方法失去原来的引用基值:
const { sayName } = student
try {
sayName()
} catch (error) {
console.log(error.name) // TypeError
}
const boundSayName = student.sayName.bind(student)
console.log(boundSayName()) // student
简单的括号不会改变引用基值:
console.log((student.sayName)()) // student
而把方法包进另一个表达式、赋值给变量或作为裸函数传递,才会改变调用形式。
五、call、apply、bind 和 Reflect.apply
5.1 call 和 apply
call 与 apply 都会立即调用目标函数;第一个参数用于指定 this,区别在于后续参数的传递形式:
function add(first, second) {
return this.base + first + second
}
const context = { base: 10 }
console.log(add.call(context, 1, 2)) // 13
console.log(add.apply(context, [1, 2])) // 13
apply 的第二个参数需要是数组或类数组对象,call 则逐个接收参数。现代代码也可以使用展开语法:
console.log(add.call(context, ...[1, 2])) // 13
console.log(Reflect.apply(add, context, [1, 2])) // 13
Reflect.apply 是更直接的反射式调用 API;它不会把调用失败转换成特殊返回值,目标不可调用时会抛出 TypeError。
严格模式与非严格模式对 thisArg 的处理不同:
function strictValue() {
'use strict'
return this
}
function sloppyValue() {
return this
}
console.log(strictValue.call(2)) // 2
console.log(sloppyValue.call(2)) // Number 对象包装器
console.log(strictValue.call(null)) // null
console.log(sloppyValue.call(null) === globalThis) // true
5.2 bind
bind 不立即调用函数,而是创建一个新的绑定函数:
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`
}
const boundGreet = greet.bind({ name: 'Ada' }, 'Hello')
console.log(boundGreet('!')) // Hello, Ada!
绑定函数的 this 不能被第二次 bind 覆盖:
const first = greet.bind({ name: 'Ada' }, 'Hi')
const second = first.bind({ name: 'Grace' })
console.log(second('!')) // Hi, Ada!
5.3 new 会忽略绑定的 this
如果目标函数本身可构造,绑定函数也可以作为构造器使用。使用 new 时,绑定的 thisArg 被忽略,但预绑定参数仍然保留:
function Student(name, age) {
this.name = name
this.age = age
}
const BoundStudent = Student.bind({ name: 'ignored' }, 'Ada')
const student = new BoundStudent(20)
console.log(student.name) // Ada
console.log(student.age) // 20
console.log(student instanceof Student) // true
这里的 new 不是“又执行了一次 bind”;它走的是构造调用和 [[Construct]] 语义。绑定函数的构造行为会转发到目标函数。
六、构造函数调用模式
使用 new 调用普通构造函数时,this 指向新创建的实例:
function Student(name) {
this.name = name
}
const student = new Student('Ada')
console.log(student.name) // Ada
console.log(Object.getPrototypeOf(student) === Student.prototype) // true
简化理解 new:
- 创建一个新对象;
- 将新对象的
[[Prototype]]设置为构造器的prototype; - 以新对象作为
this执行构造器; - 如果构造器返回对象或函数,使用该返回值;否则返回新对象。
function ReturnPrimitive() {
this.value = 1
return 2
}
function ReturnObject() {
this.value = 1
return { value: 2 }
}
console.log(new ReturnPrimitive().value) // 1
console.log(new ReturnObject().value) // 2
箭头函数没有 [[Construct]],不能使用 new:
const ArrowStudent = () => {}
try {
new ArrowStudent()
} catch (error) {
console.log(error.name) // TypeError
}
七、箭头函数:词法 this
箭头函数没有自己的 this、arguments、super 或 new.target 绑定,也不能作为构造器。它从定义时的外层执行上下文捕获 this,因此 call、apply、bind 不能改写它的 this:
const object = {
name: 'Ada',
getArrow() {
const arrow = () => this.name
return arrow
}
}
const readName = object.getArrow()
console.log(readName()) // Ada
console.log(readName.call({ name: 'Grace' })) // Ada
这也是 class 方法中常用箭头回调的原因:
class Timer {
constructor() {
this.count = 0
}
start() {
setTimeout(() => {
this.count += 1
console.log(this.count)
}, 0)
}
}
但不要把箭头函数当作普通对象方法:
const objectWithArrow = {
name: 'Ada',
getName: () => this.name
}
// getName 的 this 不是 objectWithArrow,而是定义该对象字面量时的外层 this。
如果方法需要根据调用者变化,就使用普通方法;如果回调需要稳定捕获外层实例,就使用箭头函数或显式 bind。class 字段中的箭头函数会为每个实例创建一份函数,也有额外内存成本。
八、class 中的 this
class 构造器必须通过 new 调用,class 方法的 this 仍然取决于调用形式:
class User {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
static describe() {
return this.name
}
}
const user = new User('Ada')
console.log(user.getName()) // Ada
console.log(User.describe()) // User
const getName = user.getName
try {
getName()
} catch (error) {
console.log(error.name) // TypeError:class 方法默认严格模式
}
派生 class 的构造器在调用 super() 前不能读取 this:
class Base {}
class Derived extends Base {
constructor() {
super()
this.ready = true
}
}
super.method() 调用父类方法时,父类方法中的 this 通常仍是当前子类实例,不是父类原型对象:
class Base {
print() {
return this.name
}
}
class Derived extends Base {
constructor(name) {
super()
this.name = name
}
printFromBase() {
return super.print()
}
}
console.log(new Derived('Ada').printFromBase()) // Ada
九、DOM 事件处理函数中的 this
使用普通函数作为 addEventListener 监听器时,浏览器通常把 this 设置为当前监听器注册的元素,也就是 event.currentTarget:
<button class="button">点击</button>
<script>
const button = document.querySelector('.button')
button.addEventListener('click', function (event) {
console.log(this === event.currentTarget) // true
console.log(this === event.target) // 点击 button 时通常为 true
})
</script>
事件委托时,currentTarget 与 target 可能不同:
const list = document.querySelector('.list')
list.addEventListener('click', function (event) {
console.log(this === event.currentTarget) // true:list
console.log(event.target) // 实际被点击的 li 或其子节点
})
如果使用箭头函数,this 不会被 DOM API 设置,而是捕获外层词法值:
button.addEventListener('click', event => {
// this 不是 button;需要使用 event.currentTarget
console.log(event.currentTarget)
})
旧版 IE 的 attachEvent 曾把处理函数的 this 指向 window,但该 API 已被现代浏览器移除。新代码使用 addEventListener。
内联事件处理器
内联 onclick 属性有历史特殊语义,外层处理代码的 this 通常是元素:
<button onclick="console.log(this === event.currentTarget)">
点击我
</button>
不要在新项目中依赖内联事件属性;使用 addEventListener 可以分离结构和行为,也能明确处理器的词法环境。
十、调用表达式与 new 的解析
原文使用 new Student.doSth.call(person) 说明调用优先级。更准确地说,这个表达式不是“先执行 Student.doSth.call(person) 再把返回值交给 new”,而会按 new 的语法解析为对 Student.doSth.call 这个函数进行构造调用;Function.prototype.call 不可构造,因此会抛出 TypeError:
const Student = {
doSth() {
return this
}
}
const person = { name: 'Ada' }
try {
// 等价于尝试构造 Student.doSth.call,而不是构造 doSth.call(...) 的返回值
new Student.doSth.call(person)
} catch (error) {
console.log(error.name) // TypeError
}
如果想把 person 作为构造器的预绑定参数或 this,必须先明确意图:
function Constructor(name) {
this.name = name
}
const BoundConstructor = Constructor.bind(person, 'Ada')
const instance = new BoundConstructor()
console.log(instance.name) // Ada
console.log(instance !== person) // true:new 创建了新实例
这里更适合讨论“调用形式和语法解析”,而不是给 new、call、方法调用排一个绝对的运行时优先级。
十一、判断 this 的步骤
遇到面试题或旧代码时,可以按以下顺序分析:
- 是否是箭头函数:是的话查找定义时的外层
this,跳过普通函数绑定规则; - 是否使用
new:使用时绑定到新实例,构造器返回对象/函数时可能替换; - 是否使用
call/apply/Reflect.apply:使用传入的thisArg,再考虑目标函数是否严格; - 是否是绑定函数:普通调用使用绑定的
this,构造调用时忽略绑定的thisArg; - 是否是
obj.method():使用调用表达式左侧的对象; - 否则是裸调用:严格模式为
undefined,非严格模式通常为globalThis; - 如果是宿主回调:查阅该 API 对回调
this的规定;箭头函数和bind可以避免依赖隐式行为。
十二、总结
- 普通函数的
this主要由调用方式决定,不由定义位置决定; - 严格模式裸调用的
this是undefined,非严格模式通常是globalThis; obj.method()中的this是调用表达式左侧的对象,方法被取出后可能变成裸调用;call、apply、Reflect.apply立即调用并显式指定this,bind返回新函数;new会绑定新实例,并在构造器返回对象/函数时采用显式返回值;- 箭头函数没有自己的
this,不能用new,也不能被call/apply/bind改写; - class 默认严格模式,派生构造器需要先
super(); - DOM 普通事件监听器中的
this通常是currentTarget,箭头监听器则使用外层this; - 浏览器经典脚本、模块、Node CommonJS 和 Node ESM 的顶层
this不同,应使用globalThis进行跨环境判断。