技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

JS 的 this 指向

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/48-JS的this指向
本文目录13 个章节
  1. 一、先记住一张规则表
  2. 二、全局上下文中的 this
  3. 三、普通函数调用:默认绑定
  4. 四、对象方法调用:隐式绑定
  5. 五、call、apply、bind 和 Reflect.apply
  6. 六、构造函数调用模式
  7. 七、箭头函数:词法 this
  8. 八、class 中的 this
  9. 九、DOM 事件处理函数中的 this
  10. 十、调用表达式与 new 的解析
  11. 十一、判断 this 的步骤
  12. 十二、总结
  13. 参考资料

JS 的 this 指向

Category(分类): JavaScript Status: 已整理(2026)

原文:面试官问:JS 的 this 指向

this 的值不是由函数定义位置单独决定的。对普通函数来说,最重要的是调用形式;对箭头函数来说,this 来自定义时的词法环境;class、DOM 事件、模块和 Node.js 还会叠加宿主语境。本文保留原文的调用模式、call/apply/bindnew、箭头函数、事件处理函数和优先级主线,并修正全局、定时器和旧浏览器 API 的绝对化说法。

一、先记住一张规则表

调用或定义方式普通函数中的 this
new Fn()新创建的实例;构造器返回对象时可能被替换
fn.call(value, ...) / fn.apply(value, ...)严格模式使用 value;非严格模式会发生 this substitution
obj.fn()obj,也就是调用表达式左侧的引用基值
fn()严格模式为 undefined;非严格模式通常为 globalThis
箭头函数不创建自己的 this,捕获外层词法 this
class 方法和普通方法一样取决于调用者,但 class 默认严格模式
DOM 普通事件监听器通常是 event.currentTarget

这张表不是按照代码缩进判断,而是按照函数真正被调用的表达式判断:

'use strict'

function getName() {
  return this?.name
}

const first = { name: 'first', getName }
const second = { name: 'second', getName }

console.log(first.getName()) // first
console.log(second.getName()) // second

const detached = first.getName
console.log(detached()) // undefined:严格模式下是 undefined

同一个函数被不同对象调用时,this 可以不同。函数“被定义在哪里”与函数“如何被调用”是两个问题。

二、全局上下文中的 this

原文说“严格模式和非严格模式中全局 this 都是 window”,需要补充运行方式:

运行方式顶层 this 的典型值
浏览器经典 <script>window,也就是 globalThis
浏览器 <script type="module">undefined
Node.js CommonJS 文件module.exports(Node 的模块包装语境)
Node.js ESM 文件undefined

因此跨环境代码应优先使用 globalThis,不要假设一定存在 window

console.log(globalThis)

if (typeof window !== 'undefined') {
  console.log('当前环境提供 window')
}

经典浏览器脚本中的顶层 var 还会成为全局对象属性,而 letconstclass 不会:

<script>
  var varValue = 'var value'
  let letValue = 'let value'

  console.log(window.varValue) // var value
  console.log(window.letValue) // undefined
</script>

这是全局环境记录的属性绑定规则,不是 this 的通用规律。模块顶层默认严格模式,并且模块顶层 thisundefined

三、普通函数调用:默认绑定

3.1 非严格模式

在非严格脚本中,普通函数直接调用时,如果没有显式的调用者,this substitution 会把 undefined/null 替换为 globalThis,原始值则包装成对应的对象:

// 这段示例应放在非严格脚本中运行
function nonStrictThis() {
  return this
}

console.log(nonStrictThis() === globalThis) // true(非严格脚本)
console.log(nonStrictThis.call(null) === globalThis) // true
console.log(Object.prototype.toString.call(nonStrictThis.call(1)))
// [object Number]

不要把“非严格模式的普通函数 this 一定是 window”扩展到所有宿主:浏览器的全局对象是 window,Node.js 等环境应使用 globalThis 这一标准名称。

3.2 严格模式

严格模式不会做 this substitution:

function strictThis() {
  'use strict'
  return this
}

console.log(strictThis()) // undefined
console.log(strictThis.call(null)) // null
console.log(strictThis.call(1)) // 1:原始值不会被包装

原文中直接访问严格模式普通函数里的 this.name 会抛出 TypeError,因为 thisundefined

function readName() {
  'use strict'
  return this.name
}

try {
  readName()
} catch (error) {
  console.log(error.name) // TypeError
}

ES modules、class 的方法和 class 构造器默认处于严格模式。

3.3 回调函数不是一种特殊的 this 绑定

把函数作为参数传给另一个函数后,this调用这个回调的 API决定。数组迭代方法通常以未绑定的方式调用回调;如果需要回调中的 this,使用 API 提供的 thisArg 或显式绑定:

function logThis() {
  'use strict'
  return this
}

console.log([1].map(logThis)[0]) // undefined
console.log([1].map(logThis, { name: 'context' })[0])
// { name: 'context' }

不要简单地把所有回调都类比为 fn.call(undefined);定时器、DOM 事件、Node.js EventEmitter 等宿主/API 可能规定不同的回调接收者。最稳妥的方式是使用箭头函数捕获外层 this,或使用 bind

class Counter {
  value = 0

  start() {
    setTimeout(() => {
      this.value += 1
    }, 0)
  }
}

在浏览器中,经典 setTimeout 的普通回调通常以 windowthis;Node.js 定时器的回调接收者实现细节不同。业务代码不应依赖这种差异。

四、对象方法调用:隐式绑定

当调用形式是 object.method() 时,普通函数中的 this 是调用表达式左侧的对象:

const person = {
  name: 'Ada',
  sayName() {
    return this.name
  },
  child: {
    name: 'Grace',
    sayName() {
      return this.name
    }
  }
}

console.log(person.sayName()) // Ada
console.log(person.child.sayName()) // Grace

真正重要的是“谁在调用”,而不是方法最初被定义在哪个对象上:

function sayName() {
  'use strict'
  return this.name
}

const first = { name: 'first', sayName }
const second = { name: 'second', sayName }

console.log(first.sayName()) // first
console.log(second.sayName()) // second

4.1 把方法取出来会改变调用形式

const student = {
  name: 'student',
  sayName() {
    'use strict'
    return this.name
  }
}

const studentSayName = student.sayName
try {
  studentSayName()
} catch (error) {
  console.log(error.name) // TypeError:严格模式裸调用的 this 是 undefined
}
console.log(studentSayName.call(student)) // student

赋值、解构、传递回调都可能让方法失去原来的引用基值:

const { sayName } = student
try {
  sayName()
} catch (error) {
  console.log(error.name) // TypeError
}

const boundSayName = student.sayName.bind(student)
console.log(boundSayName()) // student

简单的括号不会改变引用基值:

console.log((student.sayName)()) // student

而把方法包进另一个表达式、赋值给变量或作为裸函数传递,才会改变调用形式。

五、callapplybindReflect.apply

5.1 callapply

callapply 都会立即调用目标函数;第一个参数用于指定 this,区别在于后续参数的传递形式:

function add(first, second) {
  return this.base + first + second
}

const context = { base: 10 }

console.log(add.call(context, 1, 2)) // 13
console.log(add.apply(context, [1, 2])) // 13

apply 的第二个参数需要是数组或类数组对象,call 则逐个接收参数。现代代码也可以使用展开语法:

console.log(add.call(context, ...[1, 2])) // 13
console.log(Reflect.apply(add, context, [1, 2])) // 13

Reflect.apply 是更直接的反射式调用 API;它不会把调用失败转换成特殊返回值,目标不可调用时会抛出 TypeError

严格模式与非严格模式对 thisArg 的处理不同:

function strictValue() {
  'use strict'
  return this
}

function sloppyValue() {
  return this
}

console.log(strictValue.call(2)) // 2
console.log(sloppyValue.call(2)) // Number 对象包装器
console.log(strictValue.call(null)) // null
console.log(sloppyValue.call(null) === globalThis) // true

5.2 bind

bind 不立即调用函数,而是创建一个新的绑定函数:

function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`
}

const boundGreet = greet.bind({ name: 'Ada' }, 'Hello')
console.log(boundGreet('!')) // Hello, Ada!

绑定函数的 this 不能被第二次 bind 覆盖:

const first = greet.bind({ name: 'Ada' }, 'Hi')
const second = first.bind({ name: 'Grace' })

console.log(second('!')) // Hi, Ada!

5.3 new 会忽略绑定的 this

如果目标函数本身可构造,绑定函数也可以作为构造器使用。使用 new 时,绑定的 thisArg 被忽略,但预绑定参数仍然保留:

function Student(name, age) {
  this.name = name
  this.age = age
}

const BoundStudent = Student.bind({ name: 'ignored' }, 'Ada')
const student = new BoundStudent(20)

console.log(student.name) // Ada
console.log(student.age) // 20
console.log(student instanceof Student) // true

这里的 new 不是“又执行了一次 bind”;它走的是构造调用和 [[Construct]] 语义。绑定函数的构造行为会转发到目标函数。

六、构造函数调用模式

使用 new 调用普通构造函数时,this 指向新创建的实例:

function Student(name) {
  this.name = name
}

const student = new Student('Ada')
console.log(student.name) // Ada
console.log(Object.getPrototypeOf(student) === Student.prototype) // true

简化理解 new

  1. 创建一个新对象;
  2. 将新对象的 [[Prototype]] 设置为构造器的 prototype
  3. 以新对象作为 this 执行构造器;
  4. 如果构造器返回对象或函数,使用该返回值;否则返回新对象。
function ReturnPrimitive() {
  this.value = 1
  return 2
}

function ReturnObject() {
  this.value = 1
  return { value: 2 }
}

console.log(new ReturnPrimitive().value) // 1
console.log(new ReturnObject().value) // 2

箭头函数没有 [[Construct]],不能使用 new

const ArrowStudent = () => {}

try {
  new ArrowStudent()
} catch (error) {
  console.log(error.name) // TypeError
}

七、箭头函数:词法 this

箭头函数没有自己的 thisargumentssupernew.target 绑定,也不能作为构造器。它从定义时的外层执行上下文捕获 this,因此 callapplybind 不能改写它的 this

const object = {
  name: 'Ada',
  getArrow() {
    const arrow = () => this.name
    return arrow
  }
}

const readName = object.getArrow()
console.log(readName()) // Ada
console.log(readName.call({ name: 'Grace' })) // Ada

这也是 class 方法中常用箭头回调的原因:

class Timer {
  constructor() {
    this.count = 0
  }

  start() {
    setTimeout(() => {
      this.count += 1
      console.log(this.count)
    }, 0)
  }
}

但不要把箭头函数当作普通对象方法:

const objectWithArrow = {
  name: 'Ada',
  getName: () => this.name
}

// getName 的 this 不是 objectWithArrow,而是定义该对象字面量时的外层 this。

如果方法需要根据调用者变化,就使用普通方法;如果回调需要稳定捕获外层实例,就使用箭头函数或显式 bind。class 字段中的箭头函数会为每个实例创建一份函数,也有额外内存成本。

八、class 中的 this

class 构造器必须通过 new 调用,class 方法的 this 仍然取决于调用形式:

class User {
  constructor(name) {
    this.name = name
  }

  getName() {
    return this.name
  }

  static describe() {
    return this.name
  }
}

const user = new User('Ada')
console.log(user.getName()) // Ada
console.log(User.describe()) // User

const getName = user.getName
try {
  getName()
} catch (error) {
  console.log(error.name) // TypeError:class 方法默认严格模式
}

派生 class 的构造器在调用 super() 前不能读取 this

class Base {}

class Derived extends Base {
  constructor() {
    super()
    this.ready = true
  }
}

super.method() 调用父类方法时,父类方法中的 this 通常仍是当前子类实例,不是父类原型对象:

class Base {
  print() {
    return this.name
  }
}

class Derived extends Base {
  constructor(name) {
    super()
    this.name = name
  }

  printFromBase() {
    return super.print()
  }
}

console.log(new Derived('Ada').printFromBase()) // Ada

九、DOM 事件处理函数中的 this

使用普通函数作为 addEventListener 监听器时,浏览器通常把 this 设置为当前监听器注册的元素,也就是 event.currentTarget

<button class="button">点击</button>
<script>
  const button = document.querySelector('.button')

  button.addEventListener('click', function (event) {
    console.log(this === event.currentTarget) // true
    console.log(this === event.target) // 点击 button 时通常为 true
  })
</script>

事件委托时,currentTargettarget 可能不同:

const list = document.querySelector('.list')

list.addEventListener('click', function (event) {
  console.log(this === event.currentTarget) // true:list
  console.log(event.target) // 实际被点击的 li 或其子节点
})

如果使用箭头函数,this 不会被 DOM API 设置,而是捕获外层词法值:

button.addEventListener('click', event => {
  // this 不是 button;需要使用 event.currentTarget
  console.log(event.currentTarget)
})

旧版 IE 的 attachEvent 曾把处理函数的 this 指向 window,但该 API 已被现代浏览器移除。新代码使用 addEventListener

内联事件处理器

内联 onclick 属性有历史特殊语义,外层处理代码的 this 通常是元素:

<button onclick="console.log(this === event.currentTarget)">
  点击我
</button>

不要在新项目中依赖内联事件属性;使用 addEventListener 可以分离结构和行为,也能明确处理器的词法环境。

十、调用表达式与 new 的解析

原文使用 new Student.doSth.call(person) 说明调用优先级。更准确地说,这个表达式不是“先执行 Student.doSth.call(person) 再把返回值交给 new”,而会按 new 的语法解析为对 Student.doSth.call 这个函数进行构造调用;Function.prototype.call 不可构造,因此会抛出 TypeError

const Student = {
  doSth() {
    return this
  }
}

const person = { name: 'Ada' }

try {
  // 等价于尝试构造 Student.doSth.call,而不是构造 doSth.call(...) 的返回值
  new Student.doSth.call(person)
} catch (error) {
  console.log(error.name) // TypeError
}

如果想把 person 作为构造器的预绑定参数或 this,必须先明确意图:

function Constructor(name) {
  this.name = name
}

const BoundConstructor = Constructor.bind(person, 'Ada')
const instance = new BoundConstructor()

console.log(instance.name) // Ada
console.log(instance !== person) // true:new 创建了新实例

这里更适合讨论“调用形式和语法解析”,而不是给 newcall、方法调用排一个绝对的运行时优先级。

十一、判断 this 的步骤

遇到面试题或旧代码时,可以按以下顺序分析:

  1. 是否是箭头函数:是的话查找定义时的外层 this,跳过普通函数绑定规则;
  2. 是否使用 new:使用时绑定到新实例,构造器返回对象/函数时可能替换;
  3. 是否使用 call/apply/Reflect.apply:使用传入的 thisArg,再考虑目标函数是否严格;
  4. 是否是绑定函数:普通调用使用绑定的 this,构造调用时忽略绑定的 thisArg
  5. 是否是 obj.method():使用调用表达式左侧的对象;
  6. 否则是裸调用:严格模式为 undefined,非严格模式通常为 globalThis
  7. 如果是宿主回调:查阅该 API 对回调 this 的规定;箭头函数和 bind 可以避免依赖隐式行为。

十二、总结

  • 普通函数的 this 主要由调用方式决定,不由定义位置决定;
  • 严格模式裸调用的 thisundefined,非严格模式通常是 globalThis
  • obj.method() 中的 this 是调用表达式左侧的对象,方法被取出后可能变成裸调用;
  • callapplyReflect.apply 立即调用并显式指定 thisbind 返回新函数;
  • new 会绑定新实例,并在构造器返回对象/函数时采用显式返回值;
  • 箭头函数没有自己的 this,不能用 new,也不能被 call/apply/bind 改写;
  • class 默认严格模式,派生构造器需要先 super()
  • DOM 普通事件监听器中的 this 通常是 currentTarget,箭头监听器则使用外层 this
  • 浏览器经典脚本、模块、Node CommonJS 和 Node ESM 的顶层 this 不同,应使用 globalThis 进行跨环境判断。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS