# 一文了解this指向

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的时候干了啥

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的时候干了啥 也是有讲到的,不太清楚的童鞋可以跳过去看一看 我们再来一个例子巩固下

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的理解,达到事半功倍的效果