一、基础概念题
1. this 是什么?为什么说 this 是在函数调用时确定而非定义时?
一句话答案:this 是函数执行时的执行上下文引用,指向当前函数运行时的”所有者”或调用者;它并非在函数定义时确定,而是在函数被调用那一刻,依据调用方式动态确定——同一个函数,定义位置不变,调用方式不同,this 指向可以完全不同,这是 this 最核心也最容易踩坑的特性。
(1) this 是什么
this 是 JavaScript 中的一个关键字,属于函数执行时执行上下文(Execution Context)的组成部分,指向当前函数运行时的上下文对象。
简单理解:this 就是”当前函数是谁在调用”或者”当前函数在谁的上下文里运行”。
function sayHi() {
console.log(this.name)
}
const person = { name: '张三', sayHi }
const animal = { name: '猫咪', sayHi }
person.sayHi() // '张三',this 指向 person
animal.sayHi() // '猫咪',this 指向 animal
sayHi() // undefined,this 指向全局/undefined
同一个函数sayHi,定义只有一份,但用三种方式调用,this 指向三个不同的对象。这就是 this 的动态性。
(2) 为什么 this 在调用时确定,而不是定义时
this 的设计目的,是让函数能够动态地引用不同的上下文对象,从而实现代码复用。一个函数可以在不同的对象上下文中执行,操作不同对象的数据,而不需要为每个对象写一份函数。
// 定义一个通用函数
function introduce() {
console.log(`我是${this.name},今年${this.age}岁`)
}
// 不同对象复用同一个函数
const person1 = { name: '张三', age: 25, introduce }
const person2 = { name: '李四', age: 30, introduce }
person1.introduce() // 我是张三,今年25岁
person2.introduce() // 我是李四,今年30岁
如果 this 在定义时就确定了,那 introduce 函数的 this 永远是定义时的那个对象,就无法复用到 person1 和 person2 上了。this 的动态绑定正是为了支持这种”函数复用 + 动态上下文”的模式。
(3) 对比:定义时确定 vs 调用时确定
| 特性 | 定义时确定(词法) | 调用时确定(动态) |
|---|---|---|
| 确定时机 | 函数定义时 | 函数调用时 |
| 是否可变 | 不可变,固定 | 可变,不同调用方式指向不同 |
| 典型代表 | 箭头函数的 this、作用域链 | 普通函数的 this |
| 优点 | 可预测,不会变 | 灵活,函数可复用 |
| 缺点 | 不够灵活 | 容易混乱,踩坑多 |
普通函数的 this 是调用时确定(动态绑定),箭头函数的 this 是定义时确定(词法绑定)——这是两者最核心的区别。
(4) this 确定的完整过程
当一个函数被调用时,JS 引擎会:
- 创建新的执行上下文
- 根据调用方式确定 this 的值(应用四大绑定规则)
- 把 this 绑定到执行上下文
- 执行函数体,函数内访问 this 时就用这个绑定值
function fn() {
console.log(this)
}
// 调用方式1:独立调用 → 默认绑定 → this = window/undefined
fn()
// 调用方式2:对象方法调用 → 隐式绑定 → this = obj
const obj = { fn }
obj.fn()
// 调用方式3:call 调用 → 显式绑定 → this = 指定对象
fn.call({ name: '显式' })
// 调用方式4:new 调用 → new 绑定 → this = 新创建的实例
new fn()
同一个函数,四种调用方式,四个不同的 this。
(5) this 的本质:函数调用的”隐式参数”
可以把 this 理解为函数调用时的一个隐式参数,JS 引擎根据调用方式自动传入:
// 可以这样类比理解(不是真实语法):
function fn(this) { // this 是一个隐式参数
console.log(this)
}
fn(window) // 独立调用,隐式传入 window
obj.fn(obj) // 方法调用,隐式传入 obj
fn.call(custom, custom) // call 调用,显式传入 custom
new fn(newInstance) // new 调用,隐式传入新实例
this 就像一个隐藏的第一个参数,调用时自动传入,函数内可以访问。
为什么 this 容易踩坑:
- 动态性:调用方式不同,this 就不同,容易忘记当前是哪种调用方式
- 回调丢失:函数作为回调传递时,调用方式变了,this 就丢了
- 严格模式差异:严格模式和非严格模式默认绑定不同
- 箭头函数特殊:箭头函数 this 是词法绑定,规则不同
- 嵌套函数:嵌套的普通函数 this 不是外层的,容易混淆
一句话总结:this 是函数执行时的执行上下文引用,指向当前函数的调用者/所有者;它在函数被调用的那一刻根据调用方式动态确定(不是定义时),同一个函数用不同方式调用 this 可以完全不同——可以把 this 理解为函数调用时的”隐式参数”,JS 引擎根据调用方式自动传入,这种动态性让函数可以复用到不同对象上下文,但也导致了 this 指向混乱和各种踩坑。
2. this 的四大绑定规则是什么?优先级顺序?
一句话答案:this 有四大绑定规则,按优先级从高到低排列:new 绑定(new 调用)> 显式绑定(call/apply/bind)> 隐式绑定(对象方法调用)> 默认绑定(独立函数调用)——判断 this 指向时,按这个优先级从高到低检查,命中哪条规则就用哪条,这是分析任何 this 问题的通用框架。
(1) 四大绑定规则总览
| 优先级 | 绑定规则 | 触发条件 | this 指向 |
|---|---|---|---|
| 1(最高) | new 绑定 | 用 new 关键字调用函数 | 新创建的实例对象 |
| 2 | 显式绑定 | 用 call/apply/bind 调用 | 指定的对象 |
| 3 | 隐式绑定 | 作为对象的方法调用 | 调用该方法的对象 |
| 4(最低) | 默认绑定 | 独立函数调用(无其他绑定) | 全局对象(非严格)/ undefined(严格) |
① 规则1:new 绑定(优先级最高)
当函数通过 new 关键字调用时,this 指向新创建的实例对象。
function Person(name) {
this.name = name // this 指向 new 创建的实例
console.log(this) // Person { name: '张三' }
}
const p = new Person('张三')
console.log(p.name) // '张三'
new 会做四件事(下一题详细讲):创建空对象 → 绑定 this → 执行函数体 → 返回实例。
② 规则2:显式绑定(call/apply/bind)
手动指定 this 指向,优先级高于隐式绑定。
const obj = { name: '显式绑定' }
function sayName() {
console.log(this.name)
}
// call:立即执行,参数列表
sayName.call(obj) // '显式绑定'
// apply:立即执行,参数数组
sayName.apply(obj) // '显式绑定'
// bind:返回新函数,永久绑定
const boundFn = sayName.bind(obj)
boundFn() // '显式绑定'
③ 规则3:隐式绑定(对象方法调用)
函数作为对象的方法调用时,this 指向调用该方法的对象(就近原则)。
const obj = {
name: '隐式绑定',
sayName() {
console.log(this.name) // this 指向 obj
}
}
obj.sayName() // '隐式绑定'
就近原则:多级嵌套对象时,this 指向最后调用方法的那个对象(最近的)。
const outer = {
name: '外层',
inner: {
name: '内层',
sayName() {
console.log(this.name) // this 指向 inner(最近的调用对象)
}
}
}
outer.inner.sayName() // '内层',this 指向 inner,不是 outer
④ 规则4:默认绑定(独立函数调用)
没有任何其他绑定规则时触发,this 指向:
- 非严格模式:全局对象(浏览器 window,Node global)
- 严格模式:undefined
// 非严格模式
function fn1() {
console.log(this) // window(浏览器)
}
fn1()
// 严格模式
function fn2() {
'use strict'
console.log(this) // undefined
}
fn2()
(2) 优先级验证:多条规则同时存在时
显式绑定 > 隐式绑定:
function sayName() {
console.log(this.name)
}
const obj1 = { name: 'obj1', sayName }
const obj2 = { name: 'obj2' }
// 隐式绑定(obj1)和显式绑定(obj2)同时存在
obj1.sayName.call(obj2) // 'obj2',显式绑定优先级更高
new 绑定 > 显式绑定:
function Person(name) {
this.name = name
}
const obj = { name: '已存在的对象' }
// bind 显式绑定到 obj
const BoundPerson = Person.bind(obj)
// 用 new 调用 bind 后的函数
const p = new BoundPerson('张三')
console.log(p.name) // '张三',new 绑定优先级更高,this 是新实例不是 obj
console.log(obj.name) // '已存在的对象',obj 没有被修改
new 绑定的优先级最高,即使函数被 bind 绑定了 this,用 new 调用时 this 仍然是新创建的实例(bind 对 new 无效)。
隐式绑定 > 默认绑定:
function sayName() {
console.log(this.name)
}
const obj = { name: 'obj', sayName }
obj.sayName() // 'obj',隐式绑定(不是默认绑定)
const fn = obj.sayName
fn() // undefined,赋值后独立调用,默认绑定
判断 this 指向的通用流程:
看到一个函数调用
↓
1. 是否用 new 调用?
├─ 是 → new 绑定,this = 新创建的实例
└─ 否 → 继续
↓
2. 是否用 call/apply/bind 调用?
├─ 是 → 显式绑定,this = 指定的对象
└─ 否 → 继续
↓
3. 是否作为对象方法调用(obj.fn())?
├─ 是 → 隐式绑定,this = 调用该方法的对象(就近)
└─ 否 → 继续
↓
4. 默认绑定
├─ 非严格模式 → this = 全局对象(window/global)
└─ 严格模式 → this = undefined
按这个流程从高到低检查,任何 this 问题都能分析清楚。
(3) 特殊情况:箭头函数
箭头函数不适用这四大绑定规则,它没有自己的 this,this 继承自定义时的外层作用域(词法绑定),且 call/apply/bind/new 都不能改变。
const arrowFn = () => {
console.log(this)
}
const obj = { name: 'obj' }
arrowFn.call(obj) // this 仍是外层的,不是 obj(显式绑定对箭头函数无效)
new arrowFn() // 报错:箭头函数不能 new
箭头函数的 this 是另一套规则(词法绑定),判断时要先区分是普通函数还是箭头函数。
一句话总结:this 四大绑定规则按优先级从高到低是new 绑定 > 显式绑定(call/apply/bind)> 隐式绑定(对象方法调用)> 默认绑定(独立调用)——判断 this 时按这个顺序从高到低检查,命中哪条用哪条;new 绑定优先级最高(即使 bind 了 new 也能覆盖),显式绑定能覆盖隐式绑定,默认绑定优先级最低;箭头函数不适用这套规则,它的 this 是词法绑定继承外层。
3. new 绑定的执行过程是什么?new 做了哪四件事?
一句话答案:用 new 调用函数时,JS 引擎会做四件事——①创建一个全新的空对象;②把这个空对象的原型(proto)指向构造函数的 prototype 属性,同时把空对象绑定为函数的 this;③执行构造函数体,this 上的属性和方法会被添加到新对象上;④如果构造函数没有显式 return 一个对象,就自动返回这个新对象(如果 return 了对象就返回那个对象,return 原始值则忽略)。
(1) new 做的四件事详解
① 第1步:创建一个全新的空对象
function Person(name) {
this.name = name
}
const p = new Person('张三')
// new 的第1步:创建空对象 {}
// 这个对象是全新的,和其他实例互不影响
每次 new 都会创建一个独立的空对象,多个实例之间是不同的对象:
const p1 = new Person('张三')
const p2 = new Person('李四')
console.log(p1 === p2) // false,两个不同的对象
② 第2步:绑定原型和 this
两件事同时发生:
- 新对象的 proto(内部原型指针)指向构造函数的 prototype 属性
- 构造函数的 this 绑定到这个新对象
function Person(name) {
this.name = name // this 就是新创建的空对象
}
Person.prototype.sayHi = function() {
console.log(`你好,我是${this.name}`)
}
const p = new Person('张三')
// 原型链建立:p.__proto__ === Person.prototype
console.log(p.__proto__ === Person.prototype) // true
// this 绑定:p 上有了 name 属性
console.log(p.name) // '张三'
// 可以访问原型上的方法
p.sayHi() // '你好,我是张三'
图解原型链:
p(新对象)
├── name: '张三'(构造函数体中 this.name = name 添加的)
└── __proto__ → Person.prototype
├── sayHi: function() {}
└── constructor: Person
③ 第3步:执行构造函数体
以新对象为 this 执行构造函数体内的代码,this 上的赋值会变成新对象的属性:
function Person(name, age) {
// this 是新对象,执行这些代码后新对象有了这些属性
this.name = name
this.age = age
this.friends = ['李四', '王五'] // 每个实例独立一份
// 也可以执行其他逻辑
console.log(`创建了一个人:${this.name}`)
}
const p = new Person('张三', 25)
// 控制台输出:创建了一个人:张三
console.log(p) // Person { name: '张三', age: 25, friends: ['李四', '王五'] }
④ 第4步:返回新对象(有特殊规则)
构造函数执行完后,返回值规则:
- 没有 return:自动返回新对象(this)
- return 一个对象:返回这个对象(而不是新对象)
- return 原始值(数字、字符串、布尔、null、undefined):忽略返回值,仍然返回新对象
// 情况1:没有 return → 返回新对象
function Person1(name) {
this.name = name
}
const p1 = new Person1('张三')
console.log(p1) // Person1 { name: '张三' } 返回新对象
// 情况2:return 一个对象 → 返回这个对象
function Person2(name) {
this.name = name
return { name: '被替换的对象', age: 99 } // 显式返回对象
}
const p2 = new Person2('张三')
console.log(p2) // { name: '被替换的对象', age: 99 } 不是新对象!
// 情况3:return 原始值 → 忽略,返回新对象
function Person3(name) {
this.name = name
return '我是字符串' // 原始值
}
const p3 = new Person3('张三')
console.log(p3) // Person3 { name: '张三' } 返回新对象,字符串被忽略
// return null 也是原始值,返回新对象
function Person4(name) {
this.name = name
return null
}
const p4 = new Person4('张三')
console.log(p4) // Person4 { name: '张三' } null 被忽略
return 对象的坑:
// 经典坑:构造函数 return 了一个对象,new 的结果不是预期的实例
function Person(name) {
this.name = name
this.sayHi = () => console.log(this.name)
return { name: '黑客' } // 不小心 return 了对象
}
const p = new Person('张三')
console.log(p.name) // '黑客',不是 '张三'!
p.sayHi() // 报错,p 上没有 sayHi 方法
实际开发中构造函数一般不要写 return,让它自动返回新对象。
(2) 手写实现一个 new(经典面试题)
理解了 new 的四步,就能手写实现一个 myNew 函数:
function myNew(constructor, ...args) {
// 第1步:创建空对象
const obj = {}
// 第2步:绑定原型和 this
obj.__proto__ = constructor.prototype // 原型链建立
// 第3步:执行构造函数体,this 绑定到 obj
const result = constructor.apply(obj, args)
// 第4步:返回值判断
// 如果构造函数返回了对象,返回那个对象;否则返回新对象
return (typeof result === 'object' && result !== null)
? result
: obj
}
// 测试
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function() {
console.log(`我是${this.name},${this.age}岁`)
}
const p = myNew(Person, '张三', 25)
console.log(p.name) // '张三'
console.log(p.age) // 25
p.sayHi() // '我是张三,25岁'
console.log(p instanceof Person) // true
更规范的写法(用 Object.create 代替直接操作proto):
function myNew(constructor, ...args) {
const obj = Object.create(constructor.prototype) // 创建对象并绑定原型
const result = constructor.apply(obj, args) // 执行构造函数
return (typeof result === 'object' && result !== null) ? result : obj
}
(3) new 绑定和其他绑定的优先级
new 绑定优先级最高,即使函数被 bind 绑定了 this,new 调用时 this 仍然是新实例:
function Person(name) {
this.name = name
}
const obj = { name: '已绑定的对象' }
const BoundPerson = Person.bind(obj) // bind 到 obj
const p = new BoundPerson('张三') // 用 new 调用
console.log(p.name) // '张三',this 是新实例,不是 obj
console.log(obj.name) // '已绑定的对象',obj 没有被修改
(4) class 中的 new
ES6 class 本质上也是构造函数的语法糖,new class 的过程和 new 函数一样:
class Person {
constructor(name) {
this.name = name // this 是新实例
}
sayHi() { // 方法挂在原型上
console.log(`我是${this.name}`)
}
}
const p = new Person('张三')
// 同样的四步:创建空对象 → 绑定原型和this → 执行constructor → 返回实例
class 不能直接调用(必须 new),否则报错:
Person('张三') // TypeError: Class constructor cannot be invoked without 'new'
一句话总结:new 调用函数时做四件事——
- 创建全新空对象;
- 空对象的proto指向构造函数的 prototype,同时 this 绑定到空对象;
- 以新对象为 this 执行构造函数体;
- 返回值规则:无 return 返回新对象,return 对象返回该对象,return 原始值忽略仍返回新对象
理解这四步就能手写实现 new,也能理解 new 绑定优先级最高(能覆盖 bind)的原因。
二、实操场景题
4. call/apply/bind 的区别和使用场景?
一句话答案:三者都是显式绑定 this 的方法——call 和 apply 都是立即执行函数,区别是 call 传参数列表(逗号分隔),apply 传参数数组;bind 不立即执行,而是返回一个永久绑定 this 的新函数,后续调用时 this 固定(new 调用除外);使用场景上 call 用于普通显式调用,apply 用于参数是数组的场景(如 Math.max 配合数组),bind 用于回调函数固定 this(如 React 事件、setTimeout 回调)。
(1) 三者对比总览
| 方法 | 是否立即执行 | 参数形式 | 返回值 | 典型场景 |
|---|---|---|---|---|
| call | 立即执行 | 参数列表 fn.call(obj, a, b, c) | 函数执行结果 | 普通显式调用、借用方法 |
| apply | 立即执行 | 参数数组 fn.apply(obj, [a, b, c]) | 函数执行结果 | 参数是数组、Math.max 配合数组 |
| bind | 不立即执行 | 预设参数 fn.bind(obj, a) | 绑定 this 的新函数 | 回调固定 this、偏函数 |
① call:立即执行 + 参数列表
function introduce(greeting, punctuation) {
console.log(`${greeting},我是${this.name}${punctuation}`)
}
const person = { name: '张三' }
// call:第一个参数是 this,后面是参数列表
introduce.call(person, '你好', '!')
// 输出:你好,我是张三!
call 的参数是一个个传的,逗号分隔。
② apply:立即执行 + 参数数组
function introduce(greeting, punctuation) {
console.log(`${greeting},我是${this.name}${punctuation}`)
}
const person = { name: '张三' }
// apply:第一个参数是 this,第二个是参数数组
introduce.apply(person, ['你好', '!'])
// 输出:你好,我是张三!
apply 的参数放在一个数组里传。
call 和 apply 的选择:
// 参数明确、数量固定 → 用 call(更直观)
fn.call(obj, a, b, c)
// 参数是数组、数量不固定 → 用 apply
const args = [1, 2, 3, 4, 5]
fn.apply(obj, args)
// ES6 之后,用展开运算符可以让 call 也能处理数组
fn.call(obj, ...args) // 等价于 fn.apply(obj, args)
apply 的经典场景:Math.max 配合数组
const numbers = [1, 5, 3, 8, 2]
// Math.max 不接受数组,只接受参数列表
Math.max(1, 5, 3, 8, 2) // 8
// 用 apply 把数组拆成参数列表
Math.max.apply(null, numbers) // 8,this 传 null 不影响
// ES6 展开运算符(更简洁,推荐)
Math.max(...numbers) // 8
③ bind:返回新函数 + 永久绑定
function introduce(greeting) {
console.log(`${greeting},我是${this.name}`)
}
const person = { name: '张三' }
// bind:返回一个新函数,this 永久绑定到 person
const boundIntroduce = introduce.bind(person)
// 调用新函数,this 固定是 person
boundIntroduce('你好') // 你好,我是张三
boundIntroduce('哈喽') // 哈喽,我是张三
// 即使换种方式调用,this 也不变
boundIntroduce.call({ name: '李四' }) // 你好,我是张三(call 改不了 bind 的 this)
bind 返回的是新函数,原函数不变:
const originalFn = function() { console.log(this) }
const boundFn = originalFn.bind({ name: '绑定对象' })
console.log(originalFn === boundFn) // false,是两个不同的函数
originalFn() // this 还是 window/undefined(原函数不受影响)
boundFn() // this 是绑定对象
bind 预设参数(偏函数):bind 不仅能绑定 this,还能预设参数
function add(a, b, c) {
return a + b + c
}
// 绑定 this 为 null,并预设第一个参数 a = 10
const addTen = add.bind(null, 10)
addTen(2, 3) // 15,相当于 add(10, 2, 3)
addTen(5, 5) // 20,相当于 add(10, 5, 5)
// 可以继续预设更多参数
const addTenAndTwenty = add.bind(null, 10, 20)
addTenAndTwenty(3) // 33,相当于 add(10, 20, 3)
这叫偏函数应用(Partial Application),把多参数函数变成少参数函数。
bind 的使用场景:
- 场景1:回调函数固定 this(最常用)
class Counter {
constructor() {
this.count = 0
// 构造函数中 bind,this 固定为实例
this.increment = this.increment.bind(this)
}
increment() {
this.count++
console.log(this.count)
}
}
const counter = new Counter()
// 作为回调传递,this 不丢失
setTimeout(counter.increment, 1000) // 1,this 还是实例
React 类组件中经典写法:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
// 每个方法都要 bind
this.handleClick = this.handleClick.bind(this)
this.handleChange = this.handleChange.bind(this)
}
handleClick() {
this.setState({ count: this.state.count + 1 })
}
}
- 场景2:setTimeout/setInterval 回调
const obj = {
name: '张三',
sayHi() {
console.log(`你好,我是${this.name}`)
}
}
// 普通函数回调:this 丢失
setTimeout(obj.sayHi, 1000) // undefined,this 不是 obj
// bind 修复:this 固定为 obj
setTimeout(obj.sayHi.bind(obj), 1000) // '你好,我是张三'
- 场景3:借用方法(call/apply)
// 借用数组方法处理类数组对象
function sum() {
// arguments 是类数组,没有 reduce 方法
// 借用 Array.prototype.reduce
return Array.prototype.reduce.call(arguments, (total, num) => total + num, 0)
}
sum(1, 2, 3, 4) // 10
// 借用 Math.max 处理数组
const arr = [1, 5, 3]
Math.max.apply(null, arr) // 5
// 借用对象的 toString 判断类型
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call({}) // '[object Object]'
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
call/apply/bind 对箭头函数无效:
const arrowFn = () => {
console.log(this.name)
}
const obj = { name: '张三' }
arrowFn.call(obj) // undefined,this 不是 obj(call 无效)
arrowFn.apply(obj) // undefined,apply 无效
const bound = arrowFn.bind(obj)
bound() // undefined,bind 也无效
箭头函数的 this 是词法绑定,call/apply/bind 都不能改变。
(2) 三者的实现原理(手写):
理解了原理,可以手写实现:
// 手写 call
Function.prototype.myCall = function(context, ...args) {
context = context || window
const fn = Symbol() // 用 Symbol 避免属性名冲突
context[fn] = this // this 是调用 myCall 的函数
const result = context[fn](...args)
delete context[fn]
return result
}
// 手写 apply
Function.prototype.myApply = function(context, args) {
context = context || window
const fn = Symbol()
context[fn] = this
const result = context[fn](...args)
delete context[fn]
return result
}
// 手写 bind(简化版)
Function.prototype.myBind = function(context, ...presetArgs) {
const fn = this
return function(...callArgs) {
return fn.apply(context, [...presetArgs, ...callArgs])
}
}
一句话总结:call/apply/bind 都是显式绑定 this 的方法——call 和 apply 立即执行,call 传参数列表(fn.call(obj, a, b)),apply 传参数数组(fn.apply(obj, [a, b]),适合数组参数如 Math.max);bind 不立即执行,返回永久绑定 this 的新函数(还能预设参数做偏函数),适合回调函数固定 this(React 事件、setTimeout);三者对箭头函数都无效——实际开发中 call 用于普通显式调用和借用方法,apply 用于数组参数场景,bind 用于回调固定 this。
2. 隐式绑定的就近原则和方法赋值丢失 this 的坑?
一句话答案:隐式绑定里,this 指向最后调用方法的那个对象(就近原则,多级嵌套时指向最近的对象);最经典的坑是方法赋值后独立调用——把对象方法赋给变量再调用,调用方式由“对象方法调用”变为“独立函数调用”,隐式绑定随之丢失,this 变成默认绑定(全局/undefined),这也是回调函数中 this 丢失的根本原因。
(1) 隐式绑定的基本规则
函数以对象方法的形式被调用时,this 指向调用该方法的对象:
const obj = {
name: '张三',
sayName() {
console.log(this.name) // this 指向 obj
}
}
obj.sayName() // '张三',this = obj
(2) 就近原则:多级嵌套对象
对象存在多级嵌套时,this 指向最后(最近)调用方法的那个对象,而非最外层对象:
const outer = {
name: '外层对象',
inner: {
name: '内层对象',
sayName() {
console.log(this.name) // this 指向谁?
}
}
}
outer.inner.sayName() // '内层对象',this 指向 inner(最近的调用对象)
(3) 为什么是 inner 不是 outer
outer.inner.sayName() 的调用过程:
- 先访问 outer.inner,得到 inner 对象
- 在 inner 对象上调用 sayName 方法
- 因此调用 sayName 的对象是 inner(最后一个 . 前面的对象)
- this 指向 inner
this 指向最后一个点号前面的对象,也就是直接调用方法的那个对象。
验证:
const a = {
name: 'a',
b: {
name: 'b',
c: {
name: 'c',
sayName() {
console.log(this.name)
}
}
}
}
a.b.c.sayName() // 'c',this 指向 c(最近的对象)
(4) 方法赋值丢失 this 的坑(最经典)
将对象的方法赋给一个变量,再独立调用这个变量,this 就会丢失:
const obj = {
name: '张三',
sayName() {
console.log(this.name)
}
}
obj.sayName() // '张三',隐式绑定,this = obj
// 把方法赋值给变量
const fn = obj.sayName
fn() // undefined,this 丢失!
// 非严格模式 this = window,window.name 是空字符串或 undefined
// 严格模式 this = undefined,直接报错
为什么赋值后 this 丢失:
- obj.sayName():调用方式是“对象方法调用”,触发隐式绑定,this = obj
- const fn = obj.sayName:只是把函数引用赋给 fn,函数本身没有变
- fn():调用方式是“独立函数调用”,没有对象前缀,触发默认绑定,this = window/undefined
赋值只是传递了函数引用,并没有绑定 this。函数的 this 由调用方式决定,与定义位置或来源无关。
图解:
obj.sayName()
↑
有对象前缀 obj → 隐式绑定 → this = obj
const fn = obj.sayName
fn()
↑
没有对象前缀 → 默认绑定 → this = window/undefined
(5) 回调函数中的 this 丢失(实际开发中最常见)
方法赋值丢失 this 正是回调函数中 this 丢失的根本原因。把方法作为回调传递,本质上就是方法赋值:
const obj = {
name: '张三',
sayHi() {
console.log(`你好,我是${this.name}`)
}
}
// setTimeout 的回调参数本质上是:
// setTimeout 内部:callback = obj.sayHi; callback()
// 相当于方法赋值后独立调用 → this 丢失
setTimeout(obj.sayHi, 1000) // 1秒后输出:你好,我是undefined
// 同理,DOM 事件回调
btn.addEventListener('click', obj.sayHi)
// 点击时 this 指向 btn(DOM 规范),不是 obj
// 同理,Promise 回调
fetch('/api').then(obj.handleResponse)
// then 内部独立调用 handleResponse,this 丢失
所有“把方法作为回调传递”的场景,都会发生 this 丢失,因为回调函数最终是被第三方独立调用的。
(6) 解决方案
方案1:外层包一层箭头函数(最常用)
setTimeout(() => {
obj.sayHi() // 箭头函数 this 继承外层,sayHi 作为 obj 方法调用,this = obj
}, 1000)
方案2:bind 绑定 this
setTimeout(obj.sayHi.bind(obj), 1000) // bind 永久绑定 this 到 obj
方案3:在对象内部用箭头函数定义方法(类字段)
const obj = {
name: '张三',
sayHi: () => { // 箭头函数 this 继承外层(全局/模块)
console.log(this.name) // 注意:对象字面量中箭头函数 this 不是 obj!
}
}
// 这种写法在对象字面量中 this 是外层的,不是 obj,要小心
// 正确用法是在 class 中用类字段箭头函数
方案4:构造函数中 bind
class Person {
constructor(name) {
this.name = name
this.sayHi = this.sayHi.bind(this) // 构造函数中绑定
}
sayHi() {
console.log(`你好,我是${this.name}`)
}
}
(7) 连续赋值的坑
const obj1 = {
name: 'obj1',
sayName() {
console.log(this.name)
}
}
const obj2 = {
name: 'obj2'
}
// 连续赋值:obj2.sayName = obj1.sayName
obj2.sayName = obj1.sayName
obj2.sayName() // 'obj2',this 指向 obj2(隐式绑定,谁调用指向谁)
// 函数是从 obj1 拿来的,但调用者是 obj2,所以 this 是 obj2
函数不记得自己来自哪个对象,只关心当前是谁在调用它。
(8) 别名调用的坑
const obj = {
name: '张三',
sayName() {
console.log(this.name)
}
}
// 别名:给方法起个别名
const alias = obj.sayName
alias() // undefined,独立调用,默认绑定
// 即使在表达式中调用
;(obj.sayName)() // '张三',括号不改变调用方式,还是 obj.sayName()
// 但逗号表达式会改变
;(false, obj.sayName)() // undefined,逗号表达式返回函数引用,独立调用
(9) 判断隐式绑定的关键:看调用时有没有对象前缀
// 有对象前缀 → 隐式绑定
obj.fn() // this = obj
obj.inner.fn() // this = obj.inner
// 没有对象前缀 → 默认绑定
fn() // this = window/undefined
const f = obj.fn; f() // this = window/undefined
// 回调函数(本质是没有对象前缀的独立调用)
setTimeout(obj.fn, 1000) // this = window/undefined
一句话总结:隐式绑定中 this 指向最后调用方法的对象(就近原则,多级嵌套指向最近的对象,即最后一个点号前面的对象);最经典的坑是方法赋值后独立调用——把 obj.fn 赋给变量后 fn(),调用方式从对象方法调用变成独立调用,隐式绑定丢失,this 变成默认绑定(全局/undefined)——这是所有回调函数(setTimeout、事件、Promise)中 this 丢失的根本原因,解决方案是外层包箭头函数、bind 绑定、或类中用箭头函数方法。
6、箭头函数的 this 为什么不能被 call/apply/bind 改变?
一句话答案:箭头函数没有自己的 this 绑定,它的 this 在定义时通过作用域链查找外层 this 确定(词法/静态绑定),就像查找普通变量一样,一旦确定就固定不变;call/apply/bind 的作用是设置函数自身的 this 绑定,而箭头函数根本没有这个绑定槽位,所以无法被改变——箭头函数的 this 本质上是外层作用域的一个“变量”,不是函数自身的属性。
(1) 普通函数的 this:可改变的绑定
普通函数在创建时,内部有一个 this 绑定机制,调用时 this 的值由调用方式决定,call/apply/bind 可以显式设置这个值:
const normalFn = function() {
console.log(this.name)
}
const obj1 = { name: '张三' }
const obj2 = { name: '李四' }
normalFn.call(obj1) // '张三',call 设置 this = obj1
normalFn.call(obj2) // '李四',call 设置 this = obj2
normalFn.apply(obj1) // '张三',apply 也能改
const bound = normalFn.bind(obj1)
bound() // '张三',bind 永久绑定
普通函数的 this 就像一个可以被赋值的变量,call/apply/bind 就是给它赋值的手段。
(2) 箭头函数的 this:不可改变的词法查找
箭头函数没有自己的 this。在箭头函数内访问 this 时,JS 引擎会像查找普通变量一样,沿着作用域链往外找:
class Person {
name = '张三'
sayHi() {
// 外层作用域(sayHi 方法)的 this = Person 实例
const arrowFn = () => {
// 箭头函数自己没有 this
// → 沿作用域链往外找
// → 找到 sayHi 作用域的 this(Person 实例)
// → 用这个 this,固定不变
console.log(this.name)
}
arrowFn()
}
}
const p = new Person()
p.sayHi() // '张三'
为什么 call/apply/bind 无效:
const arrowFn = () => {
console.log(this.name)
}
const obj = { name: '张三' }
// 尝试用 call 改变 this
arrowFn.call(obj) // undefined,不是 '张三'
// 为什么无效?
// call/apply/bind 的工作原理:
// 调用函数时,把指定的对象设置为函数内部的 this 绑定
//
// 但箭头函数根本没有”内部的 this 绑定”这个东西
// → 你无法给一个不存在的东西赋值
// → 箭头函数的 this 是从外层作用域查找的变量
// → call/apply/bind 改变不了外层作用域的变量
// → 所以完全无效
类比理解:
// 普通函数的 this 像一个”参数”,call/apply 可以传入不同的值
const normalFn = function() {
console.log(this) // this 像一个特殊参数,调用时决定
}
normalFn.call(obj1) // 传入 obj1 作为 this
normalFn.call(obj2) // 传入 obj2 作为 this
// 箭头函数的 this 像一个”外层变量”,call/apply 改变不了
const outerVar = this // 假设外层有个变量
const arrowFn = () => {
console.log(outerVar) // outerVar 是外层变量,调用方式改变不了它
}
arrowFn.call(obj1) // outerVar 还是外层的,不会变成 obj1
箭头函数的 this 本质上就是外层作用域的一个变量,call/apply 只能改变函数自身的 this 绑定,改变不了外层变量。
(3) 验证:各种方式都改不了箭头函数的 this
const arrowFn = () => {
console.log(this === window ? 'window' : this)
}
const obj = { name: '测试对象' }
// call 改不了
arrowFn.call(obj) // window(或模块作用域的 this)
// apply 改不了
arrowFn.apply(obj) // window
// bind 改不了
const bound = arrowFn.bind(obj)
bound() // window
// 即使 bind 后再 call,也改不了
bound.call({ name: '另一个对象' }) // window
// new 调用直接报错(箭头函数不能 new)
try {
new arrowFn()
} catch (e) {
console.log(e.message) // arrowFn is not a constructor
}
(4) call/apply/bind 对箭头函数不是完全无效
虽然不能改变 this,但参数传递和偏函数应用仍然有效:
const add = (a, b) => a + b
// call 传参有效
add.call(null, 1, 2) // 3,this 传 null 被忽略,参数正常传递
// apply 传参有效
add.apply(null, [1, 2]) // 3
// bind 预设参数有效(偏函数)
const addTen = add.bind(null, 10)
addTen(5) // 15,相当于 add(10, 5)
call/apply/bind 对箭头函数:
- 改变 this:无效
- 传递参数:有效
- 偏函数应用(bind 预设参数):有效
箭头函数 this 的查找过程(作用域链):
function outer() {
// outer 的 this 由调用方式决定
const middle = () => {
// middle 没有自己的 this,找 outer 的 this
const inner = () => {
// inner 没有自己的 this,找 middle 的 this
// middle 也没有,继续找 outer 的 this
console.log(this) // 最终用 outer 的 this
}
inner()
}
middle()
}
outer.call({ name: '外层this' }) // { name: '外层this' }
// 所有嵌套箭头函数的 this 都是 outer 的 this
箭头函数的 this 会一直沿作用域链往外找,直到找到一个有 this 的普通函数作用域(或全局作用域),然后用那个 this。
(5) 实际开发中的影响:
好处:this 固定,不用 bind
class App {
count = 0
// 箭头函数方法:this 永远是实例
handleClick = () => {
this.count++ // this 正确,不用 bind
}
}
坑:对象方法不能用箭头函数
const obj = {
name: '张三',
sayName: () => {
console.log(this.name) // this 不是 obj,是外层(全局/模块)
}
}
obj.sayName() // undefined
坑:DOM 事件回调需要 this 指向元素时不能用箭头函数
btn.addEventListener('click', () => {
this.style.color = 'red' // this 不是 btn
})
// 用 e.target 替代
btn.addEventListener('click', (e) => {
e.target.style.color = 'red' //
})
一句话总结:箭头函数没有自己的 this 绑定,它的 this 是定义时沿作用域链查找外层 this 确定的(词法/静态绑定),就像查找普通变量一样,一旦确定就固定不变;call/apply/bind 的作用是设置函数自身的 this 绑定,但箭头函数根本没有这个绑定槽位,所以无法被改变——箭头函数的 this 本质是外层作用域的一个“变量”,不是函数自身的属性,call/apply/bind 改变不了外层变量(但参数传递和偏函数应用仍然有效)。
7. 事件处理函数、定时器、类方法中的 this 丢失问题和解决方案?
一句话答案:这三个场景是 this 丢失的重灾区——DOM 事件回调中普通函数 this 指向触发元素(不是外层对象),定时器回调中普通函数 this 指向全局/undefined,类方法作为回调传递时 this 丢失(从隐式绑定变成默认绑定);解决方案统一是三种:外层包箭头函数(this 继承外层)、bind 绑定 this(永久固定)、类中用箭头函数定义方法(类字段,this 永远指向实例)。
(1) 场景1:DOM 事件处理函数的 this
普通函数作为事件回调:this 指向触发元素
点击
const btn = document.getElementById('btn')
// 普通函数:this 指向触发事件的 DOM 元素(btn)
btn.addEventListener('click', function() {
console.log(this) // 点击
this.style.color = 'red' // 生效,this 是 btn 元素
})
DOM 规范规定:addEventListener 的普通函数回调中,this 指向绑定事件的元素(currentTarget)。
箭头函数作为事件回调:this 继承外层
const obj = {
name: '外层对象',
init() {
const btn = document.getElementById('btn')
// 箭头函数:this 继承外层(init 方法的 this = obj)
btn.addEventListener('click', () => {
console.log(this.name) // '外层对象',this 是 obj,不是 btn
// this.style.color = 'red' // 报错,this 不是 DOM 元素
})
}
}
obj.init()
需要访问 DOM 元素时:用 e.target / e.currentTarget
btn.addEventListener('click', (e) => {
console.log(this.name) // 外层对象的 this
e.target.style.color = 'red' // 用 e.target 访问触发元素
e.currentTarget.style.color = 'red' // 用 e.currentTarget 访问绑定元素
})
e.target vs e.currentTarget:
- e.target:触发事件的元素(可能是子元素)
- e.currentTarget:绑定事件的元素(就是 addEventListener 的那个元素)
事件回调 this 丢失的典型场景(Vue/React 组件中):
// Vue2 中
export default {
data() {
return { count: 0 }
},
mounted() {
const btn = document.getElementById('btn')
// 普通函数:this 指向 btn,不是 Vue 实例
btn.addEventListener('click', function() {
this.count++ // this 是 btn,不是 Vue 实例,count 不存在
})
// 箭头函数:this 继承外层(Vue 实例)
btn.addEventListener('click', () => {
this.count++ // this 是 Vue 实例
})
}
}
(2) 场景2:setTimeout/setInterval 的 this
普通函数作为定时器回调:this 指向全局/undefined
const obj = {
name: '张三',
sayHi() {
// 普通函数回调:this 指向全局(非严格)或 undefined(严格)
setTimeout(function() {
console.log(this.name) // undefined,this 不是 obj
}, 1000)
}
}
obj.sayHi()
为什么定时器回调 this 是全局:
setTimeout 内部执行回调的方式是独立调用 callback(),没有对象前缀,触发默认绑定,this 指向全局对象(非严格模式)或 undefined(严格模式)。
- 解决方案1:箭头函数(最常用)
const obj = {
name: '张三',
sayHi() {
// 箭头函数:this 继承外层(sayHi 的 this = obj)
setTimeout(() => {
console.log(this.name) // '张三',this 是 obj
}, 1000)
}
}
- 解决方案2:bind 绑定
const obj = {
name: '张三',
sayHi() {
setTimeout(function() {
console.log(this.name) // '张三'
}.bind(this), 1000) // bind 到外层 this
}
}
- 解决方案3:that = this(老写法,不推荐)
const obj = {
name: '张三',
sayHi() {
const that = this // 保存 this
setTimeout(function() {
console.log(that.name) // '张三',用 that
}, 1000)
}
}
setInterval 同理:
class Counter {
constructor() {
this.count = 0
}
start() {
// 箭头函数:this 固定为实例
this.timer = setInterval(() => {
this.count++
console.log(this.count)
}, 1000)
}
}
(3) 场景3:类方法作为回调的 this 丢失
普通类方法作为回调:this 丢失
class Counter {
constructor() {
this.count = 0
}
increment() {
this.count++
console.log(this.count)
}
}
const counter = new Counter()
// 直接调用:this 指向实例
counter.increment() // 1
// 作为回调传递:this 丢失
setTimeout(counter.increment, 1000)
// 1秒后报错:Cannot read property 'count' of undefined
// 因为 increment 被独立调用,this 是 undefined(严格模式)
为什么类方法作为回调 this 丢失:
- setTimeout(counter.increment, 1000) 本质上是:
- 把 counter.increment 这个函数引用传给 setTimeout
- setTimeout 内部独立调用这个函数 callback()
- 调用方式从 counter.increment()(隐式绑定)变成了 callback()(默认绑定)
- this 从 counter 实例变成了 undefined
这就是第5题讲的“方法赋值丢失 this”。
- 解决方案1:外层包箭头函数
setTimeout(() => {
counter.increment() // 箭头函数 this 继承外层,increment 作为 counter 方法调用
}, 1000)
- 解决方案2:bind 绑定
setTimeout(counter.increment.bind(counter), 1000)
// bind 返回新函数,this 永久绑定到 counter
- 解决方案3:构造函数中 bind(React 类组件经典写法)
class Counter {
constructor() {
this.count = 0
// 构造函数中把方法 bind 到实例
this.increment = this.increment.bind(this)
this.decrement = this.decrement.bind(this)
}
increment() {
this.count++
}
decrement() {
this.count--
}
}
const counter = new Counter()
setTimeout(counter.increment, 1000) // this 不丢失,因为构造函数中已经 bind 了
- 解决方案4:类字段箭头函数(最简洁,推荐)
class Counter {
count = 0
// 箭头函数作为类字段:this 永远指向实例
increment = () => {
this.count++
console.log(this.count)
}
}
const counter = new Counter()
setTimeout(counter.increment, 1000) // this 不丢失
箭头函数方法是每个实例的属性,this 在定义时就绑定到实例,无论怎么调用 this 都是实例。
React 类组件中的对比:
// 老写法:构造函数中 bind(繁琐)
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
this.handleClick = this.handleClick.bind(this) // 每个方法都要 bind
}
handleClick() {
this.setState({ count: this.state.count + 1 })
}
render() {
return 点击
}
}
// 新写法:类字段箭头函数(简洁)
class MyComponent extends React.Component {
state = { count: 0 }
handleClick = () => { // 箭头函数方法,this 固定
this.setState({ count: this.state.count + 1 })
}
render() {
return 点击
}
}
(4) 三大场景 this 指向和解决方案总结
| 场景 | 普通函数 this | 箭头函数 this | 解决方案 |
|---|---|---|---|
| DOM 事件回调 | 触发元素(btn) | 外层作用域 | 需要元素用 e.target;需要外层 this 用箭头函数 |
| setTimeout/setInterval | 全局/undefined | 外层作用域 | 箭头函数(推荐)/ bind / that=this |
| 类方法作为回调 | undefined(严格模式) | 实例(类字段箭头函数) | 外层包箭头 / bind / 构造函数bind / 类字段箭头函数 |
统一解决方案优先级:
- 箭头函数(最简洁,推荐):回调直接写成箭头函数,this 继承外层
- 类字段箭头函数:类中需要作为回调的方法直接写成箭头函数方法
- bind 绑定:需要永久绑定 this 时用 bind
- 外层包箭头函数:不能修改原方法时,外层包一层箭头函数
- that = this(老写法,不推荐):箭头函数出现之前的做法
一句话总结:事件回调、定时器、类方法回调是 this 丢失的三大重灾区——DOM 事件普通函数 this 指向触发元素(需要外层 this 时用箭头函数,需要元素时用 e.target),定时器普通函数 this 指向全局/undefined(用箭头函数或 bind),类方法作为回调 this 从隐式绑定变成默认绑定而丢失(用类字段箭头函数、bind、或外层包箭头函数)——核心原则是“需要 this 固定时用箭头函数,需要动态 this(如 DOM 元素)时用普通函数”。
三、深度理解题
8. 严格模式 vs 非严格模式下默认绑定的区别?为什么严格模式 this 是 undefined?
一句话答案:默认绑定(独立函数调用)下,非严格模式 this 指向全局对象(浏览器 window、Node globalThis),严格模式 this 是undefined;这是 ES5 引入严格模式时的有意设计——非严格模式下独立函数调用 this 指向全局是历史遗留的设计缺陷(容易意外污染全局变量),严格模式把它改成 undefined,让错误尽早暴露,避免静默污染全局。
(1) 基本区别:
// 非严格模式
function fn1() {
console.log(this) // window(浏览器)或 globalThis(Node)
}
fn1()
// 严格模式
function fn2() {
'use strict'
console.log(this) // undefined
}
fn2()
(2) 严格模式的开启方式:
// 1. 函数级严格模式:在函数体第一行写 'use strict'
function strictFn() {
'use strict'
console.log(this) // undefined
}
// 2. 全局严格模式:在脚本第一行写 'use strict'(整个脚本都严格)
'use strict'
function globalStrictFn() {
console.log(this) // undefined
}
// 3. ES6 模块自动严格模式
// import/export 的模块自动是严格模式,不需要写 'use strict'
// 也是严格模式
(3) 为什么非严格模式 this 指向全局:
这源于 JavaScript 的历史设计。早期 JS 设计时,独立函数调用被认为是”在全局上下文中执行”,因此 this 指向全局对象。这个设计导致了一个经典坑:
// 非严格模式下的经典坑:意外创建全局变量
function setName(name) {
this.name = name // this 是 window,意外创建了全局变量 name!
}
setName('张三')
console.log(window.name) // '张三',全局变量被污染了!
console.log(name) // '张三',直接能访问到
本意可能是给某个对象设置 name,但独立调用时 this 是 window,结果意外创建/修改了全局变量,而且不会报错,非常隐蔽。
(4) 严格模式为什么改成 undefined
ES5 引入严格模式时,把独立函数调用的 this 从全局对象改成了 undefined,目的是:
- 尽早暴露错误:this 是 undefined 时,访问 this.xxx 会直接报错,而不是静默操作全局对象
- 避免全局污染:不会意外创建/修改全局变量
- 更安全:防止函数被错误地独立调用时产生不可预期的副作用
// 严格模式:错误尽早暴露
'use strict'
function setName(name) {
this.name = name // this 是 undefined
}
setName('张三') // TypeError: Cannot set properties of undefined (setting 'name')
// 直接报错,不会静默污染全局
严格模式下 this 为 undefined 的其他场景:
'use strict'
// 1. 独立函数调用
function fn() {
console.log(this) // undefined
}
fn()
// 2. 立即执行函数(IIFE)
;(function() {
console.log(this) // undefined
})()
// 3. 回调函数(setTimeout 等)
setTimeout(function() {
console.log(this) // undefined(严格模式下)
}, 100)
// 4. 方法赋值后独立调用
const obj = {
fn() {
console.log(this)
}
}
const f = obj.fn
f() // undefined
(5) 非严格模式下 this 指向全局的其他场景
// 非严格模式
function fn() {
console.log(this === window) // true
}
fn()
// IIFE
;(function() {
console.log(this === window) // true
})()
// setTimeout 回调
setTimeout(function() {
console.log(this === window) // true
}, 100)
严格模式不影响其他绑定规则:严格模式只影响默认绑定(独立函数调用),其他三种绑定规则不受影响
'use strict'
// new 绑定:this 还是新实例
function Person(name) {
this.name = name // this 是新实例,不是 undefined
}
const p = new Person('张三')
console.log(p.name) // '张三'
// 显式绑定:this 还是指定对象
function sayHi() {
console.log(this.name)
}
sayHi.call({ name: '张三' }) // '张三'
// 隐式绑定:this 还是调用对象
const obj = {
name: '张三',
sayHi() {
console.log(this.name) // '张三'
}
}
obj.sayHi() // '张三'
// 只有默认绑定(独立调用)this 是 undefined
const fn = obj.sayHi
fn() // undefined,默认绑定受严格模式影响
call/apply 传 null/undefined 的区别:
function fn() {
console.log(this)
}
// 非严格模式:call/apply 传 null/undefined,this 指向全局
fn.call(null) // window
fn.call(undefined) // window
fn.apply(null) // window
// 严格模式:call/apply 传 null/undefined,this 就是 null/undefined
'use strict'
fn.call(null) // null
fn.call(undefined) // undefined
fn.apply(null) // null
非严格模式下,call/apply 传 null 或 undefined 时,this 会被替换为全局对象;严格模式下,传什么就是什么。
bind 传 null/undefined 同理:
// 非严格模式
const bound1 = fn.bind(null)
bound1() // this = window
// 严格模式
'use strict'
const bound2 = fn.bind(null)
bound2() // this = null
(6) 实际开发中的影响
现代代码基本都是严格模式:
- ES6 模块(import/export)自动严格模式
- TypeScript 编译后默认严格模式
- React/Vue 等框架的构建产物通常是严格模式
- 所以实际开发中独立函数调用 this 基本都是 undefined
避免依赖默认绑定:
- 不要写依赖 this === window 的代码
- 独立函数中不要用 this
- 需要全局对象时用 globalThis(ES2020,统一了浏览器和 Node)
用严格模式提前发现错误:
- 严格模式下 this 是 undefined,访问 this.xxx 直接报错
- 能尽早发现”函数被错误地独立调用”的问题
- 避免静默污染全局变量
(7) 全局对象的统一访问:globalThis
// 以前:不同环境全局对象名不同
// 浏览器:window
// Node:global
// Web Worker:self
// ES2020:globalThis 统一了所有环境
console.log(globalThis) // 浏览器是 window,Node 是 global,永远正确
一句话总结:默认绑定下非严格模式 this 指向全局对象(window/globalThis),严格模式 this 是 undefined——这是 ES5 严格模式的有意设计,非严格模式下独立调用 this 指向全局是历史遗留缺陷(容易意外污染全局变量且不报错),严格模式改成 undefined 让错误尽早暴露;严格模式只影响默认绑定,new/显式/隐式绑定不受影响;现代代码(ES6模块、TS、框架构建产物)基本都是严格模式,所以独立函数调用 this 通常是 undefined,实际开发中应避免依赖默认绑定,需要全局对象用 globalThis。
9. this 绑定优先级的实际判断流程?多种规则同时存在时怎么判断?
一句话答案:判断 this 指向按优先级从高到低检查——①是否 new 调用(new 绑定,this=新实例)→ ②是否 call/apply/bind 调用(显式绑定,this=指定对象)→ ③是否对象方法调用(隐式绑定,this=调用对象,就近原则)→ ④都不是则默认绑定(非严格 this=全局,严格 this=undefined);多种规则同时存在时,优先级高的覆盖优先级低的,new 绑定最高(能覆盖 bind),显式绑定能覆盖隐式绑定,箭头函数不适用这套规则(词法绑定继承外层)。
(1) 完整判断流程图
看到一个普通函数调用
↓
是箭头函数吗?
├─ 是 → 词法绑定:this = 定义时外层作用域的 this(固定不变,call/apply/bind/new都改不了)
└─ 否 → 继续按四大规则判断
↓
第1步:是否用 new 调用?(new fn())
├─ 是 → new 绑定,this = 新创建的实例对象(优先级最高,结束判断)
└─ 否 → 继续
↓
第2步:是否用 call/apply/bind 调用?
├─ 是 → 显式绑定,this = 指定的对象(结束判断)
│ (注意:call/apply 传 null/undefined 时,非严格 this=全局,严格 this=null/undefined)
└─ 否 → 继续
↓
第3步:是否作为对象方法调用?(obj.fn())
├─ 是 → 隐式绑定,this = 最后调用方法的对象(就近原则,结束判断)
│ (注意:方法赋值后独立调用不算,那是默认绑定)
└─ 否 → 继续
↓
第4步:默认绑定(独立函数调用 fn())
├─ 非严格模式 → this = 全局对象(window/globalThis)
└─ 严格模式 → this = undefined
(2) 多种规则同时存在的判断
① 情况1:new 绑定 vs 显式绑定(new 优先级更高)
function Person(name) {
this.name = name
}
const obj = { name: '已绑定的对象' }
// bind 显式绑定到 obj
const BoundPerson = Person.bind(obj)
// 用 new 调用 bind 后的函数
const p = new BoundPerson('张三')
console.log(p.name) // '张三',new 绑定优先级更高,this 是新实例
console.log(obj.name) // '已绑定的对象',obj 没有被修改
new 绑定优先级最高,即使函数被 bind 绑定了 this,new 调用时 this 仍然是新创建的实例。
② 情况2:显式绑定 vs 隐式绑定(显式优先级更高)
function sayName() {
console.log(this.name)
}
const obj1 = { name: 'obj1', sayName }
const obj2 = { name: 'obj2' }
// 隐式绑定(obj1)和显式绑定(obj2)同时存在
obj1.sayName.call(obj2) // 'obj2',显式绑定覆盖隐式绑定
obj1.sayName.apply(obj2) // 'obj2',apply 同理
const bound = obj1.sayName.bind(obj2)
bound() // 'obj2',bind 同理
③ 情况3:隐式绑定 vs 默认绑定(隐式优先级更高)
function sayName() {
console.log(this.name)
}
const obj = { name: 'obj', sayName }
// 隐式绑定
obj.sayName() // 'obj'
// 方法赋值后独立调用 → 默认绑定(不是隐式绑定)
const fn = obj.sayName
fn() // undefined,默认绑定
方法赋值后调用方式变了,从隐式绑定变成默认绑定。
④ 情况4:连续赋值后的隐式绑定
const obj1 = {
name: 'obj1',
sayName() {
console.log(this.name)
}
}
const obj2 = {
name: 'obj2'
}
// 把 obj1 的方法赋给 obj2
obj2.sayName = obj1.sayName
obj2.sayName() // 'obj2',隐式绑定,this 指向调用者 obj2
// 函数不记得来源,只关心当前谁在调用
⑤ 情况5:多级嵌套的隐式绑定(就近原则)
const a = {
name: 'a',
b: {
name: 'b',
c: {
name: 'c',
sayName() {
console.log(this.name)
}
}
}
}
a.b.c.sayName() // 'c',this 指向最近的调用对象 c
this 指向最后一个点号前面的对象。
⑥ 情况6:call/apply 传 null/undefined
function fn() {
console.log(this)
}
// 非严格模式:传 null/undefined → this = 全局
fn.call(null) // window
fn.call(undefined) // window
fn.apply(null) // window
// 严格模式:传 null/undefined → this = null/undefined
'use strict'
fn.call(null) // null
fn.call(undefined) // undefined
⑦ 情况7:bind 后再 call/apply
function fn() {
console.log(this.name)
}
const obj1 = { name: 'obj1' }
const obj2 = { name: 'obj2' }
const bound = fn.bind(obj1) // bind 到 obj1
bound.call(obj2) // 'obj1',call 不能覆盖 bind 的 this
bound.apply(obj2) // 'obj1',apply 也不能
bound.bind(obj2)() // 'obj1',再次 bind 也不能
bind 是永久绑定,一旦绑定就不能被 call/apply/再次 bind 改变(除了 new)。
⑧ 情况8:箭头函数的 this(不适用四大规则)
const obj = {
name: 'obj',
normalFn() {
// 普通函数:this = obj(隐式绑定)
const arrowFn = () => {
// 箭头函数:this 继承外层(normalFn 的 this = obj)
console.log(this.name) // 'obj'
}
arrowFn()
// 即使 call 也改不了箭头函数的 this
arrowFn.call({ name: '其他' }) // 'obj',call 无效
},
arrowMethod: () => {
// 对象方法用箭头函数:this 继承外层(全局/模块),不是 obj
console.log(this.name) // undefined
}
}
obj.normalFn() // 'obj'
obj.arrowMethod() // undefined
箭头函数的 this 是词法绑定,不适用四大绑定规则,判断时要先区分是普通函数还是箭头函数。
(3) 实战练习:判断以下代码的 this
// 练习1
const obj = {
name: '张三',
sayHi() {
console.log(this.name)
}
}
obj.sayHi() // ? → '张三',隐式绑定
// 练习2
const fn = obj.sayHi
fn() // ? → undefined,默认绑定(方法赋值后独立调用)
// 练习3
function Person(name) {
this.name = name
this.sayHi = function() {
console.log(this.name)
}
}
const p = new Person('李四')
p.sayHi() // ? → '李四',隐式绑定(p 调用 sayHi)
// 练习4
setTimeout(p.sayHi, 1000) // ? → undefined,默认绑定(回调独立调用)
// 练习5
setTimeout(() => p.sayHi(), 1000) // ? → '李四',箭头函数 this 继承外层,sayHi 作为 p 方法调用
// 练习6
p.sayHi.call({ name: '王五' }) // ? → '王五',显式绑定覆盖隐式绑定
// 练习7
const bound = p.sayHi.bind({ name: '赵六' })
bound() // ? → '赵六',显式绑定
new bound() // ? → 报错?不,bound 是普通函数可以 new,但 this 是新实例不是赵六
// 实际上 new bound() 会创建新实例,this 是新实例(new 绑定覆盖 bind)
(4) 判断 this 的口诀
一看箭头非箭头,二看 new 不 new,三看 call/apply/bind,四看谁在调用,最后默认全局/undefined。
- 箭头函数:this = 外层作用域的 this(固定)
- new 调用:this = 新实例
- call/apply/bind:this = 指定对象
- 对象方法调用:this = 调用对象(就近)
- 独立调用:this = 全局(非严格)/ undefined(严格)
一句话总结:判断 this 按优先级从高到低——先区分是否箭头函数(词法绑定继承外层,不适用四大规则),普通函数则按 new 绑定 > 显式绑定(call/apply/bind)> 隐式绑定(对象方法调用,就近原则)> 默认绑定(独立调用,非严格全局/严格 undefined)的顺序检查;多种规则同时存在时优先级高的覆盖低的——new 能覆盖 bind,显式能覆盖隐式,bind 后 call/apply 不能再改(除了 new),方法赋值后独立调用从隐式变成默认——按这个流程任何 this 问题都能分析清楚。
10. Vue/React 中 this 的特殊处理?为什么 Vue methods 里 this 自动指向实例?TS 严格模式检测 this?
一句话答案:Vue2 中 methods、computed、watch 里的 this 自动指向组件实例,是因为 Vue 内部在调用这些函数时用 call/apply 显式绑定了 this 到组件实例(不是魔法,是框架手动绑定);React 类组件中 this 不会自动绑定,需要手动 bind 或用箭头函数方法;TypeScript 严格模式(strict: true 或 noImplicitThis: true)会在编译时检测 this 类型错误,this 类型不明确时报错,提前发现 this 指向问题。
(1) Vue2 中 this 自动指向实例的原理
// Vue2 组件
export default {
data() {
return { name: '张三', count: 0 }
},
methods: {
increment() {
this.count++ // this 自动是组件实例,为什么?
console.log(this.name) // '张三'
}
},
mounted() {
this.increment() // this 也是组件实例
}
}
(2) 为什么 Vue methods 里 this 自动是组件实例
Vue 在内部创建组件实例时,会把 methods 里的函数绑定到组件实例上。大致原理:
// Vue 内部简化实现(伪代码)
function initMethods(vm, methods) {
for (const key in methods) {
// 把方法绑定到组件实例 vm 上
vm[key] = methods[key].bind(vm) // 用 bind 把 this 永久绑定到 vm
// 或者调用时用 call:methods[key].call(vm, ...args)
}
}
Vue 把每个 method 都用 .bind(vm) 绑定到组件实例,所以无论怎么调用(直接调用、作为回调),this 永远是组件实例。
(3) 验证:Vue methods 作为回调 this 不丢失
export default {
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
console.log(this.count)
}
},
mounted() {
// 作为 setTimeout 回调,this 不丢失!
setTimeout(this.increment, 1000) // 1秒后输出 1,this 还是组件实例
// 因为 Vue 内部已经把 increment bind 到组件实例了
}
}
这是 Vue 和 React 的一个重要区别:Vue methods 自动绑定 this,React 类组件方法不自动绑定。
(4) Vue 中不能用箭头函数的场景
export default {
data() {
return { name: '张三' }
},
methods: {
// 错误:methods 用箭头函数,this 不是组件实例
sayHi: () => {
console.log(this.name) // undefined,this 是外层(模块/全局)
},
// 正确:用普通函数,Vue 自动绑定 this
sayHello() {
console.log(this.name) // '张三',this 是组件实例
}
},
// 错误:data 用箭头函数,this 不是组件实例
// data: () => ({ name: this.xxx }) // this 不对
// 正确:data 用普通函数
data() {
return { name: '张三' }
},
// computed 同理,不能用箭头函数
computed: {
// 错误
// upperName: () => this.name.toUpperCase()
// 正确
upperName() {
return this.name.toUpperCase()
}
}
}
Vue 的 data、methods、computed、watch 等选项中,函数不能用箭头函数,因为箭头函数 this 继承外层,不是组件实例,Vue 的自动绑定对箭头函数无效(call/apply/bind 改不了箭头函数的 this)。
(5) Vue 中推荐用箭头函数的场景
export default {
data() {
return { userInfo: null }
},
methods: {
fetchUser() {
// 回调函数用箭头函数,this 继承外层(组件实例)
axios.get('/api/user').then(res => {
this.userInfo = res.data // this 是组件实例
})
setTimeout(() => {
console.log(this.userInfo) // this 是组件实例
}, 1000)
}
}
}
Vue 中的原则:选项函数(methods/computed/data/watch)用普通函数(Vue 自动绑定 this),回调函数(Promise/setTimeout/事件)用箭头函数(继承外层 this)。
(6) React 类组件中 this 的处理
React 类组件不会自动绑定方法的 this,需要手动处理:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick() {
this.setState({ count: this.state.count + 1 }) // this 会丢失!
}
render() {
// 直接传方法,点击时 this 丢失,报错
return 点击
}
}
(7) React 类组件绑定 this 的四种方式
// 方式1:构造函数中 bind(最传统)
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
this.handleClick = this.handleClick.bind(this) // 构造函数中 bind
}
handleClick() {
this.setState({ count: this.state.count + 1 })
}
render() {
return 点击
}
}
// 方式2:JSX 中用箭头函数包裹(每次 render 创建新函数,有性能影响)
class MyComponent extends React.Component {
handleClick() {
this.setState({ count: this.state.count + 1 })
}
render() {
return this.handleClick()}>点击
}
}
// 方式3:JSX 中用 bind(每次 render 创建新函数,有性能影响)
class MyComponent extends React.Component {
handleClick() {
this.setState({ count: this.state.count + 1 })
}
render() {
return 点击
}
}
// 方式4:类字段箭头函数(最简洁,推荐)
class MyComponent extends React.Component {
state = { count: 0 }
handleClick = () => { // 类字段箭头函数,this 永远指向实例
this.setState({ count: this.state.count + 1 })
}
render() {
return 点击
}
}
(8) React 函数组件(Hooks):没有 this
现代 React 推荐函数组件 + Hooks,没有类,没有 this 问题:
function MyComponent() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1) // 没有 this,用 Hook
}
return 点击
}
(9) Vue3 组合式 API:没有 this
Vue3 推荐组合式 API(setup),也没有 this:
import { ref, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++ // 没有 this,用 ref
}
onMounted(() => {
console.log(count.value) // 没有 this
})
return { count, increment }
}
}
现代框架的趋势是消除 this(Vue3 组合式 API、React 函数组件),从根本上避免 this 指向问题。
(10) TypeScript 严格模式检测 this
TypeScript 的 noImplicitThis 选项(包含在 strict: true 中)会在编译时检测 this 的类型,this 类型不明确时报错:
// tsconfig.json
{
”compilerOptions”: {
”strict”: true, // 包含 noImplicitThis
”noImplicitThis”: true // 或者单独开启
}
}
检测示例:
// 错误:this 类型不明确
function sayHi() {
console.log(this.name) // TS 报错:'this' implicitly has type 'any'
// 因为这个函数独立调用时 this 是 undefined,TS 不知道 this 是什么类型
}
// 正确:显式声明 this 类型
function sayHi(this: { name: string }) {
console.log(this.name) // this 类型明确,不报错
}
const obj = { name: '张三', sayHi }
obj.sayHi() // this 是 obj,类型匹配
sayHi() // TS 报错:The 'this' context of type 'void' is not assignable
// 因为独立调用时 this 是 undefined,不是 { name: string }
类中的 this 检测:
class Counter {
count = 0
increment() {
this.count++ // this 类型是 Counter,不报错
}
// TS 会检测:作为回调传递时 this 可能丢失
start() {
setTimeout(this.increment, 1000)
// TS 报错:Type 'void' is not assignable to type 'Counter'
// 因为 setTimeout 回调独立调用时 this 是 undefined,不是 Counter
}
}
箭头函数方法的 this 检测:
class Counter {
count = 0
// 箭头函数方法:this 类型明确为 Counter
increment = () => {
this.count++ // this 是 Counter,不报错
}
start() {
setTimeout(this.increment, 1000)
// 不报错,箭头函数 this 固定为 Counter
}
}
TS 中 this 参数的用法:
// 显式声明 this 类型,TS 会在调用时检查
function toggle(this: HTMLButtonElement) {
this.classList.toggle('active')
}
const btn = document.querySelector('button')
btn.addEventListener('click', toggle) // this 是 HTMLButtonElement
toggle() // TS 报错,独立调用 this 不是 HTMLButtonElement
TS 的 this 检测能在编译时发现很多 this 指向错误,避免运行时才报错。
(11) Vue/React/TS 中 this 总结
| 框架/模式 | this 处理 | 推荐做法 |
|---|---|---|
| Vue2 选项式 | methods/computed/data 自动 bind 到实例 | 选项函数用普通函数,回调用箭头函数 |
| Vue3 组合式 | 没有 this | 用 ref/reactive,完全不用 this |
| React 类组件 | 不自动 bind,需手动处理 | 类字段箭头函数(推荐)或构造函数 bind |
| React 函数组件 | 没有 this | 用 Hooks,完全不用 this |
| TypeScript 严格模式 | 编译时检测 this 类型 | 开启 strict,提前发现 this 错误 |
(12) 现代趋势:消除 this
Vue3 组合式 API 和 React 函数组件都在消除 this,因为 this 的动态绑定是很多 bug 的来源。但理解 this 的原理仍然很重要——因为你会维护大量 Vue2 和 React 类组件的老代码,也会在面试中被问到。
一句话总结:Vue2 中 ethods/computed/data 里的 this 自动指向组件实例,是因为 Vue 内部用 bind/call 手动绑定了(不是魔法),所以选项函数必须用普通函数(箭头函数绑定无效),回调函数推荐用箭头函数;React 类组件不自动绑定 this,需要手动 bind 或用类字段箭头函数;现代框架(Vue3 组合式 API、React 函数组件)都在消除 this,从根本上避免 this 问题;TypeScript 严格模式(noImplicitThis)会在编译时检测 this 类型,this 不明确时报错,能提前发现 this 指向错误——理解 this 原理对维护老代码和面试都很重要。
四、回顾总结
10道题做完,感觉怎么样?
this 是 JavaScript 中最容易踩坑也最常考的知识点之一。核心要掌握:this 是执行上下文引用、在调用时确定(不是定义时)、四大绑定规则及优先级(new > 显式 > 隐式 > 默认)、new 绑定的四步执行过程、call/apply/bind 的区别和使用场景、隐式绑定的就近原则和方法赋值丢失 this 的坑、箭头函数 this 不能被 call/apply/bind 改变的本质、事件/定时器/类方法三大 this 丢失场景和解决方案、严格模式 vs 非严格模式默认绑定的区别、this 优先级的实际判断流程、Vue/React 中 this 的特殊处理和 TS 严格模式检测。把这些搞清楚,this 就再也难不倒你了。
核心知识点回顾:
| 模块 | 关键记忆点 |
|---|---|
| this 本质 | 执行上下文引用,调用时确定(非定义时),像隐式参数 |
| 四大规则 | new绑定 > 显式绑定(call/apply/bind) > 隐式绑定(对象方法) > 默认绑定(独立调用) |
| new 四步 | 创建空对象 → 绑定原型和this → 执行函数体 → 返回新对象(return对象则返回该对象) |
| call/apply/bind | call传参数列表立即执行、apply传参数数组立即执行、bind返回新函数永久绑定 |
| 隐式绑定坑 | 就近原则(多级嵌套指向最近对象)、方法赋值后独立调用this丢失(回调丢失的根本原因) |
| 箭头函数this | 词法绑定继承外层、无自身this绑定、call/apply/bind/new都改不了(但参数传递有效) |
| 三大丢失场景 | DOM事件(普通函数this=元素)、定时器(普通函数this=全局/undefined)、类方法回调(隐式变默认) |
| 严格模式 | 默认绑定this=undefined(非严格=全局),设计目的是尽早暴露错误避免全局污染,只影响默认绑定 |
| 判断流程 | 先区分箭头函数(词法)→普通函数按new>显式>隐式>默认从高到低检查,命中哪条用哪条 |
| Vue/React | Vue2选项函数自动bind到实例(用普通函数)、React类组件需手动bind或箭头方法、现代框架消除this、TS严格模式编译时检测this类型 |
ID:4626