JavaScript中this指向解析

137 阅读10分钟

让人困惑的this

this是JavaScript中的一个关键字,在JavaScript中当函数执行时,this会默认的传递给函数,代表着函数调用相关联的对象,通常称之为函数的上下文,但是这个this通常让人感觉飘忽不定,对于this绑定机制不了解的同学来说,真的很让人困惑,也许你见过这样的代码

    // 奇怪的that
    function fn(){
        const that = this
        return function(){
            // this.xxx = xxx 有些时候这里这么写会报错,你需要像下面那样使用
            that.xxx = xxx 
        }
    }
    
    //或者如果你使用或react 在class组件中可能有这样的写法
    <div onClick={this.handleClick.bind(this)}></div>

如果你不明白上述写法的意义,或者不了解this的绑定机制,你一定对其困惑不已,如果你有这样的感觉,阅读本文将帮助你了解JavaScript中的this绑定规则

影响this绑定的因素

在详细解释this的绑定规则之前,我们需要先了解一下js中的函数,因为this是函数执行时,会被默认传入函数的隐式参数,因此this的绑定规则和函数的定义方式执行位置相关

首先我们讲定义方式:

通常情况下我们日常开发中使用的定义方式就是以下两种

  1. 一般函数
    function fn(){
        // ....
    }
  1. 箭头函数(ES6新增)
    const fn = ()=>{
        // ...
    }

在调用方式上有以下四种

  1. 作为函数调用

作为函数调用就是我们最常见的一般调用方式

  1. 作用方法调用 当函数作为对象的属性时,该函数就称为该对象的方法
    const fn = function(){}
    const obj = {
        fn: fn
    }
    obj.fn()
  1. 构造函数new调用
    const Person = function(){}
    const person = new Person()
  1. call、apply调用
    const obj = {}
    const fn = function(number1,number2){}
    fn.call(obj,1,2)
    fn.apply(obj,[1,2])

这种方式比较特殊,call、apply是js内置的方法,主要用于绑定函数执行时的this,他们的功能是一样的,区别在于call第一个参数是函数需要绑定的this,如果函数需要一些参数,那么需要在第二个参数后依次输入,而apply第一个参数是和call一样的,但是第二个参数是使用数组的形式,就像上面的演示一样

回顾了函数的定义方式和调用方式,接下来我们就可以探讨,在以上这几种情况下函数的this是如何绑定的,遵循了那些绑定的规则

this的绑定规则

this的绑定规则总体而言可以分为以下四种模式:默认绑定、隐式绑定、显式绑定、new绑定,接下来我们就分别介绍这几种绑定模式,以及上述的情况分别由对应了哪种模式

默认绑定

当函数被独立调用时会使用默认绑定,默认绑定状态下,函数的this指向window对象,但是需要注意如果是处于严格模式下this会直接执行undefined

情况一: 全局普通调用

    function fn1(){
        console.log(this) // window
    }
    // 严格模式下指向undefined
    function fn2 () {
      "use strict"
      console.log(this) // undefined
    }

情况二:嵌套调用

function fn1 () {
  fn2()
}
function fn2 () {
  console.log(this) // window
}

这里记得我们最开始那个that的例子嘛,其实就是想在内部嵌套调用的时候,想要获取外部this的值,但是在这种情况下是执行的默认绑定模式,因此才使用了that作为媒介,用于访问外层的this

情况三:高阶函数 在JavaScript中函数是可以作为函数返回值和参数使用的,我们称这样的函数为高阶函数

function fn1 (fn) {
  fn()
}
function fn2 () {
  console.log(this) // window
}
fn1(fn2)

以上三种情况都是默认绑定模式,对应到我们之前说的,就是一般函数定义和作为普通函数执行的时候会使用默认绑定模式,因为函数的执行没有进行任何对象的绑定,所以此时就会采用默认绑定模式

隐式绑定模式

隐式绑定模式出现在函数作为方法被调用的时候,此时this会被绑定为调用函数的对象

情况一:对象方法调用

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

const obj = {
  fn: fn
}
fn() // window
obj.fn() //obj对象

我们能够明显的看到,当函数是全局调用的时候,this绑定规则是默认绑定,此时打印this是window,但是如果是对象调用此时this就绑定为调用的对象

情况二:属性嵌套调用

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

const obj1 = {
  name: 'obj1',
  fn: fn
}

const obj2 = {
  name: 'obj2',
  child: obj1
}

obj2.child.fn() // obj1

其实这种情况就属于情况一的一种变种,本质上还是不变的,obj2.child其实就是obj1,通过obj1来调用其fn方法,函数被调用时this还是会隐式绑定到调用对象上,也就是此时的obj1

情况三:属性赋值

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

const obj1 = {
  name: 'obj1',
  fn: fn
}

const fn2 = obj1.fn
fn2() // window

这里是将obj1的属性赋值给了新的变量fn2,然后调用fn2,此时打印的还是window,这是因为fn2此时保存的是函数fn的索引(也就是地址位),此时再去调用fn2就相当于全局调用的fn,根据我们第一条讲的,此时采用的是默认绑定规则,因此也就是this指向window了,也有一种说法叫隐式丢失,因为此时相当于丢失了this

总结来说隐式绑定模式是我们之前提到的函数四种调用方式中,作为对象方法被调用时使用的

显式绑定

显式绑定模式基于隐式绑定的场景之上,即手动指定绑定的this,改变原有this指向,这种指定方式有时由我们主动调用一些方法指定如call、apply、bind,有些是内置的指定,如调用的第三方库、或者js内置方法,接下来一一展示

情况一:call、apply、bind(强绑定)
有时候这种绑定方式也被成为强绑定,因为是由用户手动指定,强行改变this绑定的一种模式

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

const obj1 = {
  name: 'obj1',
  fn: fn
}
const fn2 = fn.bind(obj1)
fn() // window
fn.call(obj1) // obj1
obj1.fn.call(window) // window
fn2()// obj1

有关于call、apply之前我们已经简单介绍了,这里就不再介绍,这里简单对bind进行一下说明,bind是Function原型上的方法,也就是说所有的函数都继承了这个方法,他接受一个参数做为this对象,然后返回一个函数,返回的函数功能主体和原函数相同但是this会被绑定为传入的参数

情况二: js一些内置函数

定时器中函数this被绑定为window

setTimeout(() => {
  console.log(this) //window
}, 0);

dom方法绑定

const box = document.querySelector(".box")

box.onclick = function () {
  console.log(this) // box Dom对象
}

情况三: 第三方类库
记得我们之前写的react的案例么,我们在class组件中绑定方法时,需要手动绑定this,原因是react内部默认把设置给onClick的函数中的this绑定为了undefined

    <div onClick={this.handleClick.bind(this)}></div>

或者你在使用vue的时候,在方法中你总是可以通过this.的形式去访问到data中的数据,这也是vue底层已经帮助我们绑定过this了

    methods{
        fn: function(){
            console.log(this) //当前vue实例
        }
    }

总结来说显式绑定模式适用于之前我们提到的函数调用四种方式中的call、apply调用方式,另外一些就是js内置函数的一些绑定规则和第三方库的this绑定规则,其实显式this绑定大多数归根结底还是通过call、apply、bind来手动指定的,除了我们自己手动指定外,其他的就通过积累记忆就可以了

new 绑定

new绑定是this绑定中比较特殊的一种,他只会存在于new调用构造函数中

function Person (name) {
  console.log(this) //Person{}
  this.name = name  //Person{name:'jack'}
}
const person = new Person('jack')

当我们使用new关键字来调用函数时,js就会将被调用的函数视为构造函数,然后在就会在函数中隐式的创建一个新的对象,该对象和函数prototype(原型对象)相关联,其实这种关联就是该对象的原型(双下划线proto)就指向了函数的prototype,如果对原型链不是很清楚的同学,就直接理解为存在关联关系就可以了,然后这个对象就会被绑定到this对象上,最后如果构造函数没有主动调用return返回一个对象,那么构造函数就会将这个this默认返回出去

大概就可以理解为这种情况

function Person (name) {
    // 隐式创建了一个this
    this.name = name
    // 隐式返回了这个this
}

其他注意点

箭头函数

介绍完了四种默认绑定模式,还有一种我们没有提到就是,函数定义方式中的箭头函数,该定义方式是Es6提出的一种新的创建函数方式,箭头函数具有以下特点

  1. 都是匿名函数
    const fn = ()=>{}
    fn()=>{} // 报错 箭头函数没有标识符,都是匿名函数
  1. 箭头函数没有this绑定 这一点是我们这次关注的重点,箭头函数没有this绑定,因此你无法通过call、apply、bind的方式来显式指定箭头函数的this,箭头函数的this是其所定义位置的上级环境的this,如果上层还是箭头函数没有this,就继续向上找,直到找到全局window

不能显式绑定this

const fn = () => {
  console.log(this)
}
const obj = {}
const foo = fn.bind(obj)
foo()
fn.call(obj)
fn.apply(obj) // 全部都是window

this的默认指向

const obj = {
  fn1: fn1,
  fn2: fn2
}
function fn1 () {
  console.log(this) // obj
  return () => {
    console.log(this) // obj
  }
}
function fn2 () {
  console.log(this) // obj
  return function () {
    console.log(this) // window
  }
}
obj.fn1()()
obj.fn2()()

另外有了箭头函数后,类似于我们最开始那个that的例子就可以用箭头函数改写了

    function fn(){
        return ()=>{
            this.xxx = xxx //因为箭头函数的this默认就是指向其外部父环境的this
        }
    }

不同绑定模式的优先级

先直接说结论: new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

显式绑定 > 隐式绑定 > 默认绑定 这个优先级大家都很好理解,这里就不多做解释了,主要是new绑定和显式绑定进行一下演示

首先new绑定和显式绑定我们只能测试bind,因为new关键字是不能和call、apply组合使用的

    const Person =  function(){}
    const person = new Person().call() //报错

这里就只演示bind的情况

const Person = function () { console.log(this) } // Person {}
const obj = {}
const Person2 = Person.bind(obj)
const person = new Person2()

很显然bind并没有改变this的指向,new绑定规则优先级是要大于显式绑定的

显式绑定的一些特殊值

这些情况一般不会出现,但是我们还是提醒一下,在使用显式绑定的时候,如果显式绑定的this是undefined、null那么显式绑定会按照默认绑定的this去绑定,如果绑定的是基础数据类型,那么会绑定为其对应的包装对象,这是js底层自动处理的,就好像我们打印字符串的name属性,js也不会报错只是会返回undefined,本质上在我们将基础数据类型的值当做对象形式来访问的时候,js都会帮我们生成一个包装对象,调用后然后销毁。

function fn () {
  console.log(this)
}
fn.call(undefined) // window
fn.call(null) // window
fn.call(false) Boolean包装对象

结语

以上就是this绑定的全部知识,作为前端了解this绑定是非常重要的,因为this绑定是JavaScript函数部分最核心的知识点之一,在了解了this绑定后大家可以继而去学习文中提到的new、call、apply、bind的实现,这也是面试中常考的手撕题之一,再次深入继而可引出aop、偏函数、柯里化等函数式编程相关知识点.....

this相关暂时就整理到这里,后续有时间我会跟进整理其他相关知识点,能力有限若有纰漏恳请指正。