一文了解this指向

12/28/2020 JS

this 的中文意思是这,在 javascript 中指的是当前执行代码的环境对象.在非严格模式下,总是指向一个对象,在严格模式下可以是任意值.相信很多同学在看到这个 this 的时候,肯定是有点脑壳疼的.所以今天我就写了一篇有关 this 的小文章,来梳理梳理有关 this 的几种用法,希望对大家都能有所帮助

# 事件调用环境

谁触发事件,函数里面的this就指向谁

<button id="btn1">click me</button>
let btn1 = document.querySelector('#btn1')
function fn() {
  console.log(this)
}
btn1.onclick = fn

上面的代码打印出 button 按钮这个对象

# 全局环境

首先我们看下在全局环境下 this 指向谁?

console.log(this)

我们把上面的代码放到浏览器环境下执行,结果是Window对象 再打开终端 Terminal,键入node指令,进入 node 执行环境,结果是 global 新建一个index.js文件,使用node index.js运行脚本,这里的this指的是 node 的默认导出对象,我们执行如下代码

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

# 函数环境

# 单纯函数调用

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

但是在严格模式下,this 指向的就是undefined

'use strict'
function fn() {
  console.log(this)
}
fn() // undefined

可以看出单纯的调用函数时,this 指向的就是全局对象

# 对象方法调用

let obj = {
  a: 1,
  fn: function() {
    console.log(this)
  },
}
obj.fn() // {a: 1, fn: ƒ}

直接通过对象.方法的形式调用函数的时候,this指向的是调用这个方法的对象,即上面的obj对象 我们稍微改动一下上面的代码,让这个函数在对象中再深入一层,通过对象.属性.方法的形式去调用函数

let obj = {
  a: 1,
  b: {
    fn: function() {
      console.log(this)
    },
  },
}
obj.b.fn() // {fn: ƒ}

可以看到输出的结果是{fn: f},即b,可以得出结论: this 指向的是最终调用它的对象.当函数被多层对象所包含,且函数被最外层对象调用,this指向的也只是它的上一级对象 我们再来修改下代码,如下:

let obj = {
  a: 1,
  fn: function() {
    console.log(this)
  },
}
let fn2 = obj.fn

fn2() // Window

可以看出执行结果是Window对象,是不是觉得奇怪,这是为什么呢? 这是因为当我们进行let fn2 = obj.fn这步赋值操作的时候,我们将obj.fn这个函数的内存地址赋值给了fn2这个变量,而obj.fn这个函数干的事情就是console.log(this),现在我们执行的是fn2(),调用这个fn2函数的是 Window 对象.那你说,window 调用了一个函数,这个函数的任务就是打印出谁调用了它,那答案不是显而易见嘛

这次,我们还要来修改下代码,如下

function fn2() {
  console.log(this)
}
let obj = {
  a: 1,
  fn: fn2,
}
obj.fn() // {a: 1, fn: ƒ}

要解析这段代码的思路,其实和上面那段代码是一样的.obj调用fn函数,而fn函数指向的是fn2,我们可以理解其实就是obj调用fn2函数,所以执行的结果就是打印出obj

这次,我们还要来修改下代码,如下

let obj = {
  a: 1,
  fn: function() {
    setTimeout(function() {
      console.log(this)
    })
  },
}
obj.fn() // Window

那么,这又是为何呢?我们知道setTimeout是定时器,作用是在一段时间之后执行,setTimeout()这个函数的()中的是它的参数,也就是说function(){console.log(this)}这个函数其实是被当作一个参数传入setTimeout当中的.这个其实有个隐式的操作就是将function(){console.log(this)}赋值给一个假想函数f,到这里的时候,其实已经和obj这个对象无关了.然后等待定时器的时间到了以后,执行的就是f这个函数.这个f是被当作普通函数直接调用的,所以this指向了Window对象.我们再这么一想,function(){console.log(this)}这是个匿名函数啊,它都没有名字,是不能被普通对象调用的,但是Window可以调用啊. 那我们现在要是想要用console.log(this)打印出obj这个对象该怎么办呢?有两个方案:

  • 保存 this 变量
  • 箭头函数

方法 1:

let obj = {
  a: 1,
  fn: function() {
    let _self = this // 在这里保存好this,免得它到时候跑了
    setTimeout(function() {
      console.log(_self)
    })
  },
}
obj.fn() // {a: 1, fn: ƒ}

方法 2:

let obj = {
  a: 1,
  fn: function() {
    setTimeout(() => {
      console.log(this)
    })
  },
}
obj.fn() // {a: 1, fn: ƒ}

我们使用箭头函数也是可以达到同样的效果的,至于原因我们后面会再提到,这里就先跳过去了 但是setTimeout其实还有一个点容易忽略,我们之前说过在严格模式下直接调用一个函数,它的this指向undefined.但是在setTimeout方法中传入函数的时候,如果这个函数没有指定this的话,它会有自动注入全局上下文,类似于xxx.call(window)这样的操作.看下面代码

function fn() {
  'use strict'
  console.log(this)
}
setTimeout(fn) // Window

当然,如果我们在setTimeout中传入函数的时候绑定了this的话,那就不会被注入全局对象

function fn() {
  'use strict'
  console.log(this)
}
setTimeout(fn.bind({})) // {}

# 构造函数调用

构造函数大家都应该比较熟悉了吧,我们new一下,就 new 出一个新对象的那种.其实构造函数就是普通的函数,只不过在调用的时候前面加了new运算符.关于new运算符是干嘛的,可以看我的另外一篇文章:JS new 的时候干了啥 (opens new window)

function Person() {
  console.log(this)
}
let p = new Person() // Person {}

当我们使用构造函数调用的时候,this指向了这个实例化出来的对象,我们将上面的代码和下面的代码进行一个比对就能看出不同了

function Person() {
  console.log(this)
}
let p = Person() // Window

两段代码的唯一区别就是后面的代码没有new它,从而指向了Window对象.由此可见,使用了new之后,这个构造函数的this被绑定到了正在构造的新对象上.这个在 JS new 的时候干了啥 (opens new window) 也是有讲到的,不太清楚的童鞋可以跳过去看一看 我们再来一个例子巩固下

function Person() {
  return this
}
let p = Person()
console.log(p) // Window
console.log(p === window) // true

不使用new关键字,Person普通函数返回this,函数又是被Window调用的,所以就是返回Window对象,那么p === window返回的结果自然就是true了 但是在构造函数中,如果显式的返回了一个新的对象(非null),那么this就会指向那个对象

function Person(name) {
  this.name = name
  return {
    name: 'lisi',
  }
}
let p = new Person('zhangsan')
console.log(p.name) // lisi

# call,apply,bind 三兄弟

这三兄弟都是Function这个对象原型上的方法.它们可以更改函数中的this指向.

let obj = {}
function fn() {
  console.log(this)
}
fn() // Window
fn.call(obj) // {}
fn.apply(obj) // {}
fn.bind(obj)() // {}

这里可以看出它们三兄弟确实都可以改变this的指向,那么它们的区别在哪里呢? callapply的作用基本一致,区别在于传参的方式不太一样

let obj = {}
function fn() {
  console.log(arguments)
}
fn.call(obj, 1, 2) // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
fn.apply(obj, [1, 2]) // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]

call的参数是一个个传进去的,apply的参数是直接传了一个数组进去 那么callbind又有什么区别呢?

let obj = {}
function fn() {
  console.log(arguments)
}
fn.bind(obj)(1, 2) // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
fn.bind(obj, 1, 2)() // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]

可以看出来,绑定bind的时候是不会直接调用函数的,它会返回一个新的函数,我们需要主动去()一下,它才会执行.相对比之下,callapply都是会立即执行函数的.从第二个参数开始传入的都是执行函数时需要传入的参数,callbind传参的格式一致.另外,在构造函数调用的时候,内部其实也是有通过call来变更this指向的.这个前面我们也说到了,指向了创建出来的对象. 使用bind方法创建的上下文为永久性的上下文环境,没法更改,callapply也不行.

var a = 0
let obj = {
  a: 1,
}
function fn() {
  console.log(this.a)
}
fn() // 0
fn.call({}) // undefined  # 通过call改变了this
fn.bind(obj)() // 1  # 通过bind改变了this
fn.bind(obj).call({}) // 1  # 通过bind改变了this, call再想要来更改就没门了

上面代码中,我们是给一个普通函数指定this,下面我们来看看为对象中的方法指定this

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

换成构造函数,再来看看

function Person() {
  console.log(this)
}
let p = new Person.call({}) // Person.call is not a constructor

错误提示是 Person.call 不是一个构造函数,这是因为此时我们去new的是Person.call而不是Person,这当然不是一个构造函数了.我们换bind再来试一试,看看结果

function Person() {
  console.log(this)
}
let P = Person.bind({ a: 1 })
let p = new P() // Person {}

发现结果是Person {},这说明我们的绑定没有成功,否则结果就应该是{a:1}了.因此,我们也可以得到结论,在构造函数中,我们去new的时候,bind绑定的this是不会起效果的

# 箭头函数

箭头函数是在 ES6 的时候才有的,它的语法比普通的函数表达式要简洁,并且没有自己的thisarguments.它并不创建自身的上下文,其上下文在定义的时候就已经确定了,即一次绑定,便不可更改.

let obj = {
  fn: () => console.log(this),
}
obj.fn() //Window

这里为啥是Window而不是obj呢,箭头函数的this在定义的时候就已经确定了.在箭头函数中引用this,实际上调用的是定义时的上一层作用域的this.那么上面的代码中调用的就是Window对象了,因为obj对象是不能形成作用域的. 我们再来看下面的代码,结果也是Window,因为即使fn在函数中的位置深了一层,但是仍然没有形成作用域,箭头函数定义的时候还是指向全局对象了

let obj = {
  a: {
    fn: () => console.log(this),
  },
}
obj.a.fn() //Window

我们先看如下代码:

let obj = {
  fn: function() {
    return function() {
      console.log(this)
    }
  },
}
obj.fn()() // Window

这里函数作为对象的一个方法使用,里面有个闭包.当我们通过对象.方法去调用的时候,实际上就相当于是函数直接调用.此时的this指向Window 我们改动代码如下,将闭包的形式变成箭头函数

let obj = {
  fn: function() {
    return () => console.log(this)
  },
}
obj.fn()() // {fn: ƒ}

此时的输出结果变成obj这个对象了.因为箭头函数在定义的时候是在fn这个函数中,所以箭头函数中的this指向了fnthis,也就是obj.大家是否还记得前面有个地方我们留了一个悬念给大家.对了,就是定时器那里.我们说过,使用箭头函数也可以达到同样的效果,现在童鞋们是否都明白了.箭头函数的this在定义的时候就已经确定了,call,apply,bind等都无法改变它.

由于箭头函数的外部决定了上下文以及静态上下文等特性,因此最好不要在全局环境下使用箭头函数来定义方法,我们建议使用函数表达式来定义函数,可确保正确的上下文环境

总结: 有关this的学习到这里基本就结束了.有时间的同学建议将我文章中的代码都敲一遍,加深对this的理解,达到事半功倍的效果

    希望像星光一样闪烁
    文雀