重学 JavaScript[3] this

this 是在函数运行时进行绑定的,它的上下文取决于函数调用时的各种条件

this 的指向与函数书写位置无关,而与调用位置有关

全局的 this

浏览器环境

在浏览器环境下,this 指向Window对象:

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

这两段代码会得到同样的结果:Window对象,也就是全局对象globalThis

Node 环境

在 Node 环境下,全局对象是global,但直接打印 this 会得到一个{},实际上指向的是module.exports对象

// ... console.log(this === module.exports) // true
function fn() { console.log(this === global) } fn() // true

通过函数打印出的 this,指向global对象

绑定规则

默认绑定

上一篇中提到过使用callapply等来绑定函数以改变 this 指向

现在,我们不使用任何“绑定”的行为

this.num = 100 function fn() { console.log(this.num) } fn() // 100

不执行任何绑定行为时,this 会绑定到当前对象(在段代码里是全局对象)

请注意:如果使用严格模式,全局对象将无法使用默认绑定:

this.num = 100 function fn() { 'use strict' console.log(this.num) } fn() // TypeError报错

隐式绑定

考虑以下代码:

function fn() { console.log(this.num) } this.num = 200 let obj = { num: 100, fn: fn, } fn() obj.fn() // 200 // 100

函数 fn 被单独写出来(好像不属于 obj 对象),在对象内被引用为一个属性,当通过obj.fn()调用时,函数的 this 会根据隐式规则绑定到引用函数的上下文对象,也就是obj

如果有多个对象直接引用,形成了对象属性引用链时,只有“最接近”调用的一层会影响其调用位置:

function fn() { console.log(this.num) } let child = { num: 100, fn: fn, } let father = { num: 200, child: child, } father.child.fn() // 100

丢失

在某些时候,隐式绑定会丢失,这时候自动执行默认绑定规则,考虑以下代码:

function fn() { console.log(this.a) } function doFn(f) { f() } this.a = 'global' let obj = { a: 'obj', fn: fn, } doFn(obj.fn) // global

可以理解为:以传参方式传入的函数,其 this 也要根据实际调用情况来判定(不过往往你不知道类似doFn这样的函数的实现时要尤其小心,它很有可能改变调用时的 this)

这一点如果放在实际应用里就很好理解,调用内置的setTimeout函数:

function fn() { console.log(this.a) } this.a = 'global' let obj = { a: 'obj', fn: fn, } setTimeout(obj.fn, 100) // global

显式绑定

在上一篇我们已经介绍过显式绑定的方法

在调用call等函数时,需要传进一个thisArg参数作为绑定的对象,通常我们会传递一个类似上面提到的obj这样的复杂对象,但如果传递一个字符串、数字,或者布尔类型呢?

尝试以下代码:

function fn() { console.log(this) } fn.call('hello')

在浏览器环境运行,你会在控制台得到一个String类型的对象,而非'hello'字符串本身

再传入一个数字试试:

function fn() { console.log(this) } fn.call('hello')

也会得到类似的结果

打印出的对象形式说明:在这个过程中会对传入的原始值(基本数据类型)装箱,等价于:new String('hello')new Number(100)

new 绑定

提到 new,不得不想起上一篇中介绍的“构造器函数”,事实上 JavaScript 并不存在什么“构造器函数”,任何函数都可以被直接执行,也可以在调用前面加上new关键字(这样的调用方式被称为“构造器调用”)

比如这个函数:

function Person() { this.name = '王昭君' } let person1 = new Person() let person2 = Person() console.log(person1) console.log(person2)

第一次打印会得到一个{name: '王昭君'}的对象
第二次打印会得到undefined,这样不奇怪,因为 Person 函数什么也没返回

当一个函数被“构造器调用时”,会执行这些操作:

  • 创建一个全新的对象
  • 对这个新对象进行“原型”连接
  • 这个新对象会绑定到函数调用的 this
  • 如果函数没有返回其他对象,那么 new 表达式会自动返回这个新对象

我们主要关心第三步操作,至于其他步会在以后提到

执行构造器调用时,将函数的 this 绑定到新对象,称之为 new 绑定

绑定优先级

四种绑定优先级的关系:

默认 < 隐式 < 显式 < new

在判断时遵循优先级从大到小的顺序,如果发现有较大优先级存在,则根据较大优先级规则判断 this 指向

例外

如果你尝试了显式绑定中给出的例子,会发现如果传入的参数是null或者undefined,代码会打印出全局对象(在浏览器环境下是Window对象,在 node 环境是global对象)

function fn() { console.log(this) } fn.call('hello')

实际上这里应用的是默认绑定规则,不过正常情况下都不会去传入这两个看起来就“不正常”的值作为thisArgs

在函数柯里化(后面的文章也许会提到)操作中,调用bind时需要传入一个参数作为“占位”,如果这时候传入了这两个“不正常”的值,就有可能造成修改全局对象等不可预计的后果

箭头函数

在上一篇中我们提到了函数的几种书写方式

其中箭头函数并不是由function关键字来定义的,而是使用了 lambda 表达式来定义

箭头函数并不遵循 this 的四种绑定规则,而是根据外层作用域来决定 this

考虑以下代码:

let obj = { name: 'obj', getName1: () => { console.log(this.name) }, getName2() { this.getName1() }, getName3() { console.log(this.name) }, } this.name = 'global' obj.getName1() obj.getName2() obj.getName3() // global // global // obj

箭头函数在 this 绑定上也带来了很多优势,比如下面这段 React 代码:

class App extends React.Component { constructor() { this.state = {start: false} } handleClick() { this.setState({start: true}) } render() { return <Button onClick={ this.handleClick.bind(this) }>开始<Button/> } }

为确保事件处理函数在调用时总能得到正确的 this,在书写时要手动绑定 this

React 官网提供的示例是:

<Button onClick={() => this.handleClick()}>开始<button />

如果使用箭头函数,可以省去这一过程:

class App extends React.Component { constructor() { this.state = {start: false} } handleClick = () => { this.setState({start: true}) } render() { return <Button onClick={ this.handleClick }>开始<Button/> } }

手写 XX

在面试中经常会考察 this 绑定相关的知识,通常会要求手写callapplybind等函数,这些内容我打算在单独的一篇文章中总结

https://pwl.icu/article/1630590111390