前端基础进阶(七):全方位解读 this
Category(分类): JavaScript Status: 已整理
原文中“谁调用它,
this就指向谁”只能作为非常粗略的入门口诀。更准确的规则是:普通函数的this由调用方式决定;箭头函数没有自己的this,它从定义位置的外层词法环境获取this。class方法、ES module 和 Node.js CommonJS 还会受到宿主和严格模式的影响。
回顾执行上下文
在调用一个普通函数时,JavaScript 会根据调用形式建立执行上下文,并确定该函数调用中的 this 值。this 不是像普通变量那样沿作用域链查找的标识符;它与函数如何被调用有关。

上图是原文配图,保留作历史学习资料。现代规范不会把实现简单描述成“生成一个变量对象、作用域链和 this 指向”三个固定步骤;执行上下文、环境记录和 this 绑定是规范层面的抽象,具体引擎实现可能不同。
一、最重要的结论:普通函数的 this 由调用方式决定
const a = 10
const obj = { a: 20 }
function fn() {
console.log(this?.a)
}
fn() // 非严格脚本中可能读取全局对象的 a;严格模式/module 中 this 是 undefined
fn.call(obj) // 20
obj.fn = fn
obj.fn() // 20
同一个函数对象可以被不同方式调用,因此得到不同的 this。在函数执行过程中,this 绑定不能通过赋值改变:
function showThis() {
'use strict'
// this = {} // SyntaxError:this 不是可赋值变量
return this
}
console.log(showThis()) // undefined
二、全局代码中的 this
全局 this 必须区分脚本、模块和宿主:
// 浏览器 classic script(非 module)
console.log(this === window) // true
// ES module
// 顶层 this === undefined
// Node.js CommonJS 文件
// 顶层 this 通常是 module.exports,而不是 globalThis
浏览器的 classic script 顶层 var、顶层函数声明与 window 有特殊的全局环境绑定关系,但 let、const、class 不会作为 window 的同名属性创建。不要把“全局对象”“全局环境记录”“模块顶层”混为一谈。
更通用地访问当前宿主的全局对象,可以使用 globalThis:
console.log(globalThis)
跨 realm 的补充
每个 iframe、Worker 或独立全局环境都有自己的 realm、内建对象和全局对象。跨 realm 传递函数时,函数的 this 仍由调用方式决定;非严格函数在需要把 undefined 转成全局对象时,使用的是被调用函数所在的 realm,而不是调用者随意指定的全局对象。跨 origin iframe 还会受到同源策略限制。
// 浏览器中:foreignArray 不是当前 realm 的 Array 实例,
// 但 Array.isArray(foreignArray) 仍然为 true。
const iframe = document.createElement('iframe')
document.body.append(iframe)
const foreignArray = new iframe.contentWindow.Array(1, 2)
console.log(foreignArray instanceof Array) // false
console.log(Array.isArray(foreignArray)) // true
三、直接调用普通函数
在严格模式下,普通函数独立调用时 this 是 undefined;在非严格脚本中,undefined/null 的 this 会被转换为全局对象,原始值也会发生相应的装箱处理。
function strictFn() {
'use strict'
return this
}
function sloppyFn() {
return this
}
console.log(strictFn()) // undefined
// classic non-strict script 中:sloppyFn() === globalThis
ES module 和 class 方法默认采用严格模式,因此现代代码不应把独立调用的 this 记成永远是 window。
四、对象方法调用
当调用表达式的形式是 object.method() 时,this 通常是左侧的 object:
const foo = {
a: 10,
getA() {
return this.a
},
}
console.log(foo.getA()) // 10
但函数引用一旦被取出,调用形式就改变了:
const test = foo.getA
// strict 模式下 this 是 undefined;非严格脚本中可能转为 globalThis
console.log(test())
重点不是“函数属于哪个对象”,而是调用表达式当下是否提供了一个 base reference。下面的两种调用都不保留 foo 作为调用对象:
const method = foo.getA
method()
;(0, foo.getA)()
括号本身通常不会丢失成员调用语义:
(foo.getA)() // 仍然以 foo 作为 this
可选链也会保留正常的成员调用语义:
foo?.getA() // 如果 getA 存在,this 仍为 foo
五、嵌套函数与箭头函数
普通函数的 this 不会因为外层函数调用而自动继承:
const obj = {
a: 10,
run() {
function inner() {
return this?.a
}
return inner()
},
}
console.log(obj.run()) // 严格模式下为 undefined
箭头函数没有自己的 this、arguments、super 或 new.target,它会捕获定义位置的外层 this:
const obj = {
a: 10,
run() {
const inner = () => this.a
return inner()
},
}
console.log(obj.run()) // 10
箭头函数不能通过 call、apply 或 bind 改变 this:
const arrow = () => this
const other = { value: 1 }
console.log(arrow.call(other) === arrow()) // true
这里的 this 取决于箭头函数创建时的外层环境;不要把箭头函数写在一个 this 来源不明确的顶层位置后,再期待它在调用时动态绑定。
六、使用 call、apply 显式指定 this
call 和 apply 都会立即调用函数,区别主要在参数传递方式:
function add(num1, num2) {
return this.value + num1 + num2
}
const obj = { value: 20 }
console.log(add.call(obj, 100, 10)) // 130
console.log(add.apply(obj, [20, 10])) // 50
在现代代码中,参数数量不固定时还可以使用 Reflect.apply:
console.log(Reflect.apply(add, obj, [1, 2])) // 23
对于普通非箭头函数,call/apply 的第一个参数会参与 this 绑定;但严格模式与非严格模式对 null、undefined 和原始值的处理不同。箭头函数会忽略显式绑定。
七、常见使用场景
1. 把类数组对象转换为数组
function exam(a, b, c) {
const args = Array.prototype.slice.call(arguments)
console.log(args)
}
exam(2, 8, 9) // [2, 8, 9]
现代代码通常优先使用 Array.from 或展开语法:
function exam(...args) {
return Array.from(args)
}
console.log(exam(2, 8, 9))
2. 借用构造函数初始化实例
function Person(name, age) {
this.name = name
this.age = age
this.gender = ['man', 'woman']
}
function Student(name, age, height) {
Person.call(this, name, age)
this.height = height
}
const student = new Student('Xiaoming', 12, '150cm')
console.log(student.name, student.gender[0]) // Xiaoming man
Person.call(this, ...) 是在当前实例上执行父构造函数,并不是复制父对象的全部成员,也不会自动继承 Person.prototype 上的方法。现代代码可用 class extends 表达完整的原型关系:
class ModernPerson {
constructor(name, age) {
this.name = name
this.age = age
}
}
class ModernStudent extends ModernPerson {
constructor(name, age, height) {
super(name, age)
this.height = height
}
}
3. 异步回调中保持 this
直接把普通函数交给计时器时,调用对象不会自动保留:
const obj = {
a: 20,
getA() {
setTimeout(function () {
console.log(this?.a) // 通常不是 obj.a
}, 0)
},
}
obj.getA()
历史上常用闭包保存 this:
const obj = {
a: 20,
getA() {
const self = this
setTimeout(function () {
console.log(self.a)
}, 0)
},
}
现代代码更常用箭头函数或 bind:
const obj = {
a: 20,
getA() {
setTimeout(() => {
console.log(this.a)
}, 0)
},
}
const logger = obj.getA.bind(obj)
logger()
箭头函数适合捕获外层 this,但如果回调 API 本身通过 this 提供调用对象,就应使用普通函数。例如 DOM 事件监听器中的 this 与箭头函数不同。
八、构造函数与原型方法上的 this
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.getName = function () {
return this.name
}
const person = new Person('Nick', 20)
console.log(person.getName()) // Nick
调用 new Person(...) 时,可以用下面的教学模型理解:创建实例、让构造函数调用中的 this 指向实例、执行构造函数、建立原型关系并返回结果。若构造函数显式返回对象或函数,new 会返回该对象;显式返回原始值会被忽略。
Person.prototype.getName 上的 this 不是固定指向原型对象。person.getName() 的调用对象是 person,所以 this 是 person;如果把方法取出后独立调用,this 又会按独立调用规则处理。
const getName = person.getName
// getName() 不再以 person 为调用对象
九、class 方法、静态方法和私有字段
class 方法默认处于严格模式,调用方式仍然重要:
class Counter {
#value = 0
increment() {
this.#value++
return this.#value
}
static create() {
return new this()
}
}
const counter = Counter.create()
console.log(counter.increment()) // 1
实例方法调用时 this 是实例;静态方法调用时 this 是调用它的构造器。把 class 方法取出后直接调用通常会丢失实例:
const increment = counter.increment
// increment() 会因为 this 不是 counter 而失败
如果需要把方法作为回调传递,应显式绑定,或在合适的位置使用箭头函数包装:
button.addEventListener('click', counter.increment.bind(counter))
十、DOM 事件中的 this、target 与 currentTarget
在浏览器中,使用普通函数作为 addEventListener 回调时,规范通常把 this 设为当前事件监听器所在的 currentTarget;箭头函数没有自己的 this:
button.addEventListener('click', function (event) {
console.log(this === event.currentTarget) // true
console.log(event.target) // 实际触发事件的最内层目标
})
button.addEventListener('click', (event) => {
// 这里的 this 来自定义箭头函数的外层环境,不是 button
console.log(event.currentTarget)
})
event.target 可能是按钮内部被点击的子元素,而 event.currentTarget 是当前正在执行监听器的元素。不要用 this 替代这两个事件属性。
历史上的 IE attachEvent 行为与标准 addEventListener 不同,现代项目不需要为已淘汰的 API 编写新代码。
十一、bind 与绑定函数
const user = {
name: 'Ada',
}
function say(prefix) {
return `${prefix}, ${this.name}`
}
const boundSay = say.bind(user, 'Hello')
console.log(boundSay()) // Hello, Ada
bind 返回一个新的函数,不会立即调用原函数。绑定普通调用的 this 和预置参数只是它的一部分语义;绑定后的函数仍可能被 new 调用,此时原生 bound function 会让构造调用优先于普通 this 绑定:
function Person(name) {
this.name = name
}
const BoundPerson = Person.bind({ ignored: true }, 'Ada')
const person = new BoundPerson()
console.log(person.name) // Ada
console.log(person instanceof Person) // true
手写 bind 的面试实现往往只覆盖普通调用和预置参数,不能宣称等同原生 bind。生产代码直接使用原生方法。
十二、判断 this 的实用步骤
遇到一个 this 问题,可以按下面顺序检查:
- 它是不是箭头函数?如果是,查定义位置的外层
this。 - 它是不是通过
new调用?构造调用有更高优先级。 - 是否显式使用了
call、apply或bind?箭头函数除外。 - 调用表达式左侧是否有对象,例如
obj.method()? - 如果以上都没有,就是独立调用,再区分严格模式、module、class 和宿主。
- 如果是 DOM 事件,优先看
event.currentTarget,不要只凭this猜测。
常见的优先级口诀“new > 显式绑定 > 隐式绑定 > 默认绑定”在普通函数场景下有帮助,但不能覆盖箭头函数、class、super、代理和宿主回调等全部语义。
总结
this不是由函数声明位置决定的普通变量;普通函数的this主要由调用方式决定。- 箭头函数没有自己的
this,call/apply/bind不能改变它。 obj.method()、const method = obj.method; method()和method.call(obj)是三种不同的调用方式。- ES module、class 方法、浏览器 classic script、Node.js CommonJS 的顶层
this不同。 - 原型方法、事件监听器和构造函数中的
this都要结合实际调用表达式判断。
作者:这波能反杀 原文链接:https://www.jianshu.com/p/d647aa6d1ae6 来源:简书。原文著作权归作者所有;转载或公开发布时请遵守原作者的授权和署名要求。
参考资料: