【js基础巩固计划】深入理解this机制

174 阅读7分钟

努力让学习成为一种习惯,自信来源于充分的准备

如果你觉得该文章对你有帮助,欢迎大家点赞关注分享

前言

this的学习非常重要,无论是在我们日常开发还是面试都经常会遇到。理解this能够让你在日常开发中更加自信。但是 this的执行机制让人不太容易理解,甚至会有人把this执行机制和作用域的执行机制混淆,接下来让我们来一探究竟

奇怪的 this

我们先来看一段简单的代码

var a = 1

function outer() {
  var a = 2
  function inner() {
     console.log(a) // 2
     console.log(this.a) // 1
  }
  inner()
}

outer()

通过输出结果我们可以发现this查询变量的规则不同于作用域。在深入理解作用域与作用域链中讲到过。词法作用域在代码编译的时候就确定了的,取决于函数的声明位置。但 this不同,this是在执行上下文创建过程中生成,而执行上下文是在函数调用的时候创建的。因此this在不同的执行上下文表现形式会有所区别

全局上下文中的 this

let a = 1
var b = 2
console.log(this === window) // true
console.log(this.a) // undefined
console.log(this.b) // 2

全局上下文中,this指向全局对象window,其中通过let声明的变量不会挂载到全局对象中,因此this.aundefined。这种情况比较简单

函数执行上下文中的 this

this真正让人头疼的、并且实际运用较多的场景是在函数内

引用《js高级程序设计》中对其的定义:

在标准函数中,this引用的是把函数当成方法调用的上下文对象

换言之:函数是谁调用的,this 就指向它

ECMAScript5中会给函数增加一个属性:caller,这个属性引用的是调用当前函数的函数。如果在全局调用则为null,为了降低耦合度,也可以通过 arguments.callee.caller来引用同样的值,如下代码

function outer() {
  console.log('outer.caller :>> ', outer.caller); // null
  console.log(arguments.callee.caller) // null
  inner()
}

function inner() {
  console.log('inner.caller :>> ', inner.caller); // f outer() {xxxx}
  console.log(arguments.callee.caller) // f outer() {xxx}
}
outer()

接下来我们从绑定规则的角度来分析函数上下文中的this

默认绑定

当函数独立调用的时候采用默认绑定规则

var a = 1
function func() {
  var a = 2
  console.log(this.a) // 1
  func2()
  
}
function func2() {
  console.log(this.a) // 1
}
func()

在非严格模式下,会将全局对象用于默认绑定。在浏览器环境中,全局对象为window,因此,上面代码中 func()相当于window.func()this绑定的是window

var a = 1

function func() {
  "use strict"
  console.log(this.a) // Uncaught TypeError: Cannot read properties of undefined (reading 'a')
} 
func()

在严格模式下,则不能将全局对象用于默认绑定,this指向为undefined

隐式绑定

调用的位置是否有上下文对象

简单来说:该函数是否是作为对象的方法在调用

const obj = {
  a:1,
  func
}
function func() {
  console.log(this.a); 
}
obj.func() // 1

这里需要注意一点,func函数无论是作为外部声明后引用,还是直接定义为obj的属性,func函数都不属于 obj对象, 对象只是包含该函数的引用,即下面这种情况是一样的

const obj = {
  a:1,
  func: function() {
    console.log(this.a) 
  }
}
obj.func() // 1

隐式丢失

隐式绑定的函数容易丢失绑定对象,从而导致this的指向不符合预期

const obj = {
  a:1,
  func: function() {
    console.log(this.a) 
  }
}
const func = obj.func
func() // undefined, 严格模式下会报错

本质上,obj.funcfunc都是对函数的引用,在这个例子中。函数是独立调用。因此this采用的是默认绑定的规则

另一种更加隐蔽且常见的场景发生在回调函数中

const obj = {
  a:1,
  func: function() {
    // 业务逻辑
    // XXXX
    setTimeout(function() {
      console.log(this.a); 
    }, 0);
  }
}
obj.func() // undefined

内部函数并不会继承外层函数的this, 上面的例子中计时器中回调函数里的 this采用的是默认绑定规则,绑定的是全局对象,想让回调函数中的 this如我们预期那样,有许多种方法

  • 用一个变量保存外部函数的this:
const obj = {
  a:1,
  func: function() {
    // 业务逻辑
    // XXXX
    const _this = this
    setTimeout(function() {
      console.log(_this.a); // 1
    }, 0);
  }
}
obj.func()
  • 使用箭头函数
const obj = {
  a:1,
  func: function() {
    // 业务逻辑
    // XXXX
    setTimeout(() => {
      console.log(this.a); // 1
    }, 0);
  }
}
obj.func()

关于箭头函数不详细介绍。在这里,我们需要知道箭头函数中的this引用的是定义箭头函数的上下文,它没有自己的this

另一种常见的场景是给某个DOM元素绑定事件

<div>
    <button id="btn">button</button>
</div>

<script>
 var a = 1
 const button = document.querySelector('#btn')
 button.addEventListener('click', function () {
   console.log(this); // <button>
   console.log(this.a); // undefined
 })
  button.addEventListener('click',  () => {
   console.log(this); // window
   console.log(this.a); // 1
 })
</script>

这里监听 click事件的回调函数中的this会被强制绑定到触发事件的DOM元素中

我们可以使用箭头函数让this重新指向全局

显示绑定

如果每次都需要通过对象方法的方式调用来隐式绑定this未免太不方便了。那么有没有其他的什么方式可以在函数运行时候指定其内部的this指向呢,答案是肯定的

JS函数中分别提供了bindapplycall三种函数方法来手动设置函数执行上下文中的this

bind

const obj = {
  a: 1
}
function func() {
  console.log(this.a);
}
const _func = func.bind(obj)
_func() // 1

apply、call

const obj = {
  a: 1
}
function func(...args) {
  console.log(this.a);
  console.log('args :>> ', args); 
}
func.apply(obj, [1,2,3]) // 1 [1,2,3]
func.call(obj, 1,2,3) // 1 [1,2,3]

这里只是通过简单的例子说明bindapplycall的基本用法。本文中不具体展开介绍

额外需要注意

  1. 是如果传入的是基本类型,则this指向为其装箱类型
  2. 对箭头函数没有作用
  3. 如果传入的是nullundefined,在调用的时候会被忽略,实际采用的是默认绑定规则
var a = 3
const obj = {a: 1}
const func =  () => {
    console.log(this.a); // 3
}
func.apply(obj)

new绑定

另一种常见的this绑定是通过构造函数

function Func() {
  this.a = 1
}
const f = new Func()
console.log(f.a) // 1

有关构造函数以及new操作符的细节在这里不深入展开

我们需要知道的是: 通过 new规则方式调用的函数,会在内部创建一个新对象,并将this绑定到该对象。如果函数没有返回其他对象,则函数会默认返回这个新建的对象

优先级

隐式绑定与显式绑定

const obj1 = { a:1, func }
const obj2 = {a:2, func }
function func() {
  console.log(this.a)
}
obj1.func() // 1
obj2.func() // 2
obj1.func.apply(obj2) // 2
obj2.func.apply(obj1) // 1

通过上面的例子可以看出:显示绑定的优先级大于隐式绑定

隐式绑定与new绑定

const obj1 = {
  func: function (num) {
    this.a = num
  }
}
obj1.func(2)
console.log(obj1.a); // 2
const bar = new obj1.func(3) 
console.log(obj1.a); // 2
console.log(bar.a); // 3

通过上面的例子可以看出:new绑定的优先级大于隐式绑定

new绑定与显示绑定

function func (num) {
  this.a = num
}
const obj = {}
const _func = func.bind(obj)
_func(1)
console.log(obj.a) // 1
const bar = new _func(2)
console.log(obj.a) // 1
console.log(bar.a) // 2

通过上面的例子可以看出:new绑定的优先级大于显示绑定,new操作符直接改变了func函数里面的 this

综合下来, this绑定的优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

结语

通过上面的内容,相信大家对this机制有了更清晰的认识

最后我们可以总结下函数执行上下文中的this绑定规则(优先级):

  • 函数通过new调用。此时this为new绑定规则,绑定的是新创建的对象
const f = new func()
  • 函数通过bindapplycall方法调用,此时this为显示绑定规则,绑定的是指定对象
func.apply(obj)
  • 函数通过对象方法调用,此时this为隐式绑定规则,绑定的是上下文对象
obj.func()
  • 函数独立调用,此时this为默认绑定规则,严格模式下为undefined,非严格模式下绑定的是全局对象

到这里,就是本篇文章的全部内容了

如果你觉得该文章对你有帮助,欢迎大家点赞关注分享

如果你有疑问或者出入,评论区告诉我,我们一起讨论