JavaScript 中 this 指向机制详解与十道高频面试题解析

Feng 25 阅读 AI技术

一、基础概念题

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

Feng
这位作者很神秘,还没有填写简介。