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

显示模式

登录
ARCHIVE DOCUMENTJS

前端基础进阶(七):全方位解读 this

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/84-前端基础进阶(七):全方位解读this
本文目录14 个章节
  1. 回顾执行上下文
  2. 一、最重要的结论:普通函数的 this 由调用方式决定
  3. 二、全局代码中的 this
  4. 三、直接调用普通函数
  5. 四、对象方法调用
  6. 五、嵌套函数与箭头函数
  7. 六、使用 call、apply 显式指定 this
  8. 七、常见使用场景
  9. 八、构造函数与原型方法上的 this
  10. 九、class 方法、静态方法和私有字段
  11. 十、DOM 事件中的 this、target 与 currentTarget
  12. 十一、bind 与绑定函数
  13. 十二、判断 this 的实用步骤
  14. 总结

前端基础进阶(七):全方位解读 this

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

原文中“谁调用它,this 就指向谁”只能作为非常粗略的入门口诀。更准确的规则是:普通函数的 this调用方式决定;箭头函数没有自己的 this,它从定义位置的外层词法环境获取 thisclass 方法、ES module 和 Node.js CommonJS 还会受到宿主和严格模式的影响。

回顾执行上下文

在调用一个普通函数时,JavaScript 会根据调用形式建立执行上下文,并确定该函数调用中的 this 值。this 不是像普通变量那样沿作用域链查找的标识符;它与函数如何被调用有关。

执行上下文与 this 示意图

上图是原文配图,保留作历史学习资料。现代规范不会把实现简单描述成“生成一个变量对象、作用域链和 this 指向”三个固定步骤;执行上下文、环境记录和 this 绑定是规范层面的抽象,具体引擎实现可能不同。

一、最重要的结论:普通函数的 this 由调用方式决定

const a = 10
const obj = { a: 20 }

function fn() {
  console.log(this?.a)
}

fn() // 非严格脚本中可能读取全局对象的 a;严格模式/module 中 this 是 undefined
fn.call(obj) // 20
obj.fn = fn
obj.fn() // 20

同一个函数对象可以被不同方式调用,因此得到不同的 this。在函数执行过程中,this 绑定不能通过赋值改变:

function showThis() {
  'use strict'
  // this = {} // SyntaxError:this 不是可赋值变量
  return this
}

console.log(showThis()) // undefined

二、全局代码中的 this

全局 this 必须区分脚本、模块和宿主:

// 浏览器 classic script(非 module)
console.log(this === window) // true

// ES module
// 顶层 this === undefined

// Node.js CommonJS 文件
// 顶层 this 通常是 module.exports,而不是 globalThis

浏览器的 classic script 顶层 var、顶层函数声明与 window 有特殊的全局环境绑定关系,但 letconstclass 不会作为 window 的同名属性创建。不要把“全局对象”“全局环境记录”“模块顶层”混为一谈。

更通用地访问当前宿主的全局对象,可以使用 globalThis

console.log(globalThis)

跨 realm 的补充

每个 iframe、Worker 或独立全局环境都有自己的 realm、内建对象和全局对象。跨 realm 传递函数时,函数的 this 仍由调用方式决定;非严格函数在需要把 undefined 转成全局对象时,使用的是被调用函数所在的 realm,而不是调用者随意指定的全局对象。跨 origin iframe 还会受到同源策略限制。

// 浏览器中:foreignArray 不是当前 realm 的 Array 实例,
// 但 Array.isArray(foreignArray) 仍然为 true。
const iframe = document.createElement('iframe')
document.body.append(iframe)
const foreignArray = new iframe.contentWindow.Array(1, 2)

console.log(foreignArray instanceof Array) // false
console.log(Array.isArray(foreignArray)) // true

三、直接调用普通函数

在严格模式下,普通函数独立调用时 thisundefined;在非严格脚本中,undefined/nullthis 会被转换为全局对象,原始值也会发生相应的装箱处理。

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

function sloppyFn() {
  return this
}

console.log(strictFn()) // undefined
// classic non-strict script 中:sloppyFn() === globalThis

ES module 和 class 方法默认采用严格模式,因此现代代码不应把独立调用的 this 记成永远是 window

四、对象方法调用

当调用表达式的形式是 object.method() 时,this 通常是左侧的 object

const foo = {
  a: 10,
  getA() {
    return this.a
  },
}

console.log(foo.getA()) // 10

但函数引用一旦被取出,调用形式就改变了:

const test = foo.getA

// strict 模式下 this 是 undefined;非严格脚本中可能转为 globalThis
console.log(test())

重点不是“函数属于哪个对象”,而是调用表达式当下是否提供了一个 base reference。下面的两种调用都不保留 foo 作为调用对象:

const method = foo.getA
method()

;(0, foo.getA)()

括号本身通常不会丢失成员调用语义:

(foo.getA)() // 仍然以 foo 作为 this

可选链也会保留正常的成员调用语义:

foo?.getA() // 如果 getA 存在,this 仍为 foo

五、嵌套函数与箭头函数

普通函数的 this 不会因为外层函数调用而自动继承:

const obj = {
  a: 10,
  run() {
    function inner() {
      return this?.a
    }

    return inner()
  },
}

console.log(obj.run()) // 严格模式下为 undefined

箭头函数没有自己的 thisargumentssupernew.target,它会捕获定义位置的外层 this

const obj = {
  a: 10,
  run() {
    const inner = () => this.a
    return inner()
  },
}

console.log(obj.run()) // 10

箭头函数不能通过 callapplybind 改变 this

const arrow = () => this
const other = { value: 1 }

console.log(arrow.call(other) === arrow()) // true

这里的 this 取决于箭头函数创建时的外层环境;不要把箭头函数写在一个 this 来源不明确的顶层位置后,再期待它在调用时动态绑定。

六、使用 callapply 显式指定 this

callapply 都会立即调用函数,区别主要在参数传递方式:

function add(num1, num2) {
  return this.value + num1 + num2
}

const obj = { value: 20 }

console.log(add.call(obj, 100, 10)) // 130
console.log(add.apply(obj, [20, 10])) // 50

在现代代码中,参数数量不固定时还可以使用 Reflect.apply

console.log(Reflect.apply(add, obj, [1, 2])) // 23

对于普通非箭头函数,call/apply 的第一个参数会参与 this 绑定;但严格模式与非严格模式对 nullundefined 和原始值的处理不同。箭头函数会忽略显式绑定。

七、常见使用场景

1. 把类数组对象转换为数组

function exam(a, b, c) {
  const args = Array.prototype.slice.call(arguments)
  console.log(args)
}

exam(2, 8, 9) // [2, 8, 9]

现代代码通常优先使用 Array.from 或展开语法:

function exam(...args) {
  return Array.from(args)
}

console.log(exam(2, 8, 9))

2. 借用构造函数初始化实例

function Person(name, age) {
  this.name = name
  this.age = age
  this.gender = ['man', 'woman']
}

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

const student = new Student('Xiaoming', 12, '150cm')
console.log(student.name, student.gender[0]) // Xiaoming man

Person.call(this, ...) 是在当前实例上执行父构造函数,并不是复制父对象的全部成员,也不会自动继承 Person.prototype 上的方法。现代代码可用 class extends 表达完整的原型关系:

class ModernPerson {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
}

class ModernStudent extends ModernPerson {
  constructor(name, age, height) {
    super(name, age)
    this.height = height
  }
}

3. 异步回调中保持 this

直接把普通函数交给计时器时,调用对象不会自动保留:

const obj = {
  a: 20,
  getA() {
    setTimeout(function () {
      console.log(this?.a) // 通常不是 obj.a
    }, 0)
  },
}

obj.getA()

历史上常用闭包保存 this

const obj = {
  a: 20,
  getA() {
    const self = this
    setTimeout(function () {
      console.log(self.a)
    }, 0)
  },
}

现代代码更常用箭头函数或 bind

const obj = {
  a: 20,
  getA() {
    setTimeout(() => {
      console.log(this.a)
    }, 0)
  },
}

const logger = obj.getA.bind(obj)
logger()

箭头函数适合捕获外层 this,但如果回调 API 本身通过 this 提供调用对象,就应使用普通函数。例如 DOM 事件监听器中的 this 与箭头函数不同。

八、构造函数与原型方法上的 this

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

Person.prototype.getName = function () {
  return this.name
}

const person = new Person('Nick', 20)
console.log(person.getName()) // Nick

调用 new Person(...) 时,可以用下面的教学模型理解:创建实例、让构造函数调用中的 this 指向实例、执行构造函数、建立原型关系并返回结果。若构造函数显式返回对象或函数,new 会返回该对象;显式返回原始值会被忽略。

Person.prototype.getName 上的 this 不是固定指向原型对象。person.getName() 的调用对象是 person,所以 thisperson;如果把方法取出后独立调用,this 又会按独立调用规则处理。

const getName = person.getName
// getName() 不再以 person 为调用对象

九、class 方法、静态方法和私有字段

class 方法默认处于严格模式,调用方式仍然重要:

class Counter {
  #value = 0

  increment() {
    this.#value++
    return this.#value
  }

  static create() {
    return new this()
  }
}

const counter = Counter.create()
console.log(counter.increment()) // 1

实例方法调用时 this 是实例;静态方法调用时 this 是调用它的构造器。把 class 方法取出后直接调用通常会丢失实例:

const increment = counter.increment
// increment() 会因为 this 不是 counter 而失败

如果需要把方法作为回调传递,应显式绑定,或在合适的位置使用箭头函数包装:

button.addEventListener('click', counter.increment.bind(counter))

十、DOM 事件中的 thistargetcurrentTarget

在浏览器中,使用普通函数作为 addEventListener 回调时,规范通常把 this 设为当前事件监听器所在的 currentTarget;箭头函数没有自己的 this

button.addEventListener('click', function (event) {
  console.log(this === event.currentTarget) // true
  console.log(event.target) // 实际触发事件的最内层目标
})

button.addEventListener('click', (event) => {
  // 这里的 this 来自定义箭头函数的外层环境,不是 button
  console.log(event.currentTarget)
})

event.target 可能是按钮内部被点击的子元素,而 event.currentTarget 是当前正在执行监听器的元素。不要用 this 替代这两个事件属性。

历史上的 IE attachEvent 行为与标准 addEventListener 不同,现代项目不需要为已淘汰的 API 编写新代码。

十一、bind 与绑定函数

const user = {
  name: 'Ada',
}

function say(prefix) {
  return `${prefix}, ${this.name}`
}

const boundSay = say.bind(user, 'Hello')
console.log(boundSay()) // Hello, Ada

bind 返回一个新的函数,不会立即调用原函数。绑定普通调用的 this 和预置参数只是它的一部分语义;绑定后的函数仍可能被 new 调用,此时原生 bound function 会让构造调用优先于普通 this 绑定:

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

const BoundPerson = Person.bind({ ignored: true }, 'Ada')
const person = new BoundPerson()
console.log(person.name) // Ada
console.log(person instanceof Person) // true

手写 bind 的面试实现往往只覆盖普通调用和预置参数,不能宣称等同原生 bind。生产代码直接使用原生方法。

十二、判断 this 的实用步骤

遇到一个 this 问题,可以按下面顺序检查:

  1. 它是不是箭头函数?如果是,查定义位置的外层 this
  2. 它是不是通过 new 调用?构造调用有更高优先级。
  3. 是否显式使用了 callapplybind?箭头函数除外。
  4. 调用表达式左侧是否有对象,例如 obj.method()
  5. 如果以上都没有,就是独立调用,再区分严格模式、module、class 和宿主。
  6. 如果是 DOM 事件,优先看 event.currentTarget,不要只凭 this 猜测。

常见的优先级口诀“new > 显式绑定 > 隐式绑定 > 默认绑定”在普通函数场景下有帮助,但不能覆盖箭头函数、class、super、代理和宿主回调等全部语义。

总结

  • this 不是由函数声明位置决定的普通变量;普通函数的 this 主要由调用方式决定。
  • 箭头函数没有自己的 thiscall/apply/bind 不能改变它。
  • obj.method()const method = obj.method; method()method.call(obj) 是三种不同的调用方式。
  • ES module、class 方法、浏览器 classic script、Node.js CommonJS 的顶层 this 不同。
  • 原型方法、事件监听器和构造函数中的 this 都要结合实际调用表达式判断。

作者:这波能反杀 原文链接:https://www.jianshu.com/p/d647aa6d1ae6 来源:简书。原文著作权归作者所有;转载或公开发布时请遵守原作者的授权和署名要求。

参考资料:

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS