this 指向详解

前言

不知道大家在前端面试过程中,有没有被面试官问过关于this指向哪里,怎么改变this指向的问题,这个问题即使工作了多年的小伙伴也会一知半解,因此这篇文章详细分析了this指向的问题,让深入理解this指向。

大纲

this 指向详解_第1张图片

什么是this

函数在调用时会创建一个执行环境,this是在运行时基于函数的执行环境绑定的,它可以允许函数内部引用上下文中的执行变量,使函数编程更为优雅简洁。

来看看下面这段代码,为什么不同的调用方法打印出来的结果会不一样呢?

var a = 10

const obj = {
  a: 20,
  foo: function() {
    console.log(this.a)
    
    return function() {
      console.log(this.a)
    }
  }
}

obj.foo()() // 20 10
const fn = obj.foo
fn() // 10

其实很简单,因为这里不同调用方式的this指向不同。为什么不同的函数调用方式this就指向不同?这是由什么决定的呢?下面让我们带着疑问开始深入理解this问题吧!

this的绑定规则

默认绑定

默认绑定规则下,函数的运行环境为全局环境,this默认指向Window

默认绑定规则下的情况有以下几种

  • 全局函数this指向Window
  • 独立函数调用this指向Window
  • 自执行函数调用this指向Window
  • 闭包内部this指向window

全局函数this指向Window

当在全局函数内直接打印this,可以看到this指向为Window

img

独立函数调用,this指向Window

独立函数调用,即直接调用函数,如foo()

function foo() {
  console.log(this);
}

foo()

这里的foo被默认挂在到Window上,相当于window.foo(),根据函数隐式绑定规则,谁调用就指向谁,这里指向的this指向Window,结果如下:

this 指向详解_第2张图片
同理,如果在一个嵌套函数中直接调用的函数,也属于独立函数调用,此时this也指向Window

var a = 10
var obj = {
  a: 20,
  foo: function() {
    console.log(this); // {a: 20, foo: ƒ}
    console.log(this.a); // 20

    function son() {
      console.log(this); // Window
      console.log(this.a); // 10
    }
    // 独立函数调用
    son() 
  }
}

obj.foo()

上面代码中,对象obj中的方法foo内部还嵌套了一个子函数son,当直接调用son方法时,son内部的this指向Window,因此son内部的this.a结果为全局的变量a,即10。

那么当在son函数内部想要使用到obj中的变量a怎么办呢?很简单,直接把this对象赋值给另一个变量that,在son方法内部引用此变量即可:

var a = 10
var obj = {
  a: 20,
  foo: function() {
    const that = this

    function son() {
      console.log(that.a); // 20
    }
    son() 
  }
}

obj.foo()

自执行函数内部的this指向Window

自执行函数,顾名思义,就是函数被定义出来后就自动调用的函数,自执行函数的this指向代码如下:

// 1
(function() {
  console.log(this); // Window
})()

// 2
var a = 10 
function foo() {
  (function son(that) {
    console.log(this); // Window
    console.log(that); // {a: 20, foo: ƒ}
  })(this)
}

var obj = {
  a: 20,
  foo: foo,
}

obj.foo()

这里的foo函数内部嵌套了一个自执行函数sonson内部的this指向Window,这里this指向的原理类似独立函数调用,即先声明了一个son方法,然后再通过son()执行函数。如果想要在son中获取上一级对象obj的变量,在调用的时候,将this指向作为参数传给自执行函数son即可。

闭包内部函数的this指向Window

闭包可以理解为定义在一个函数内部的函数,是能够访问其他函数内部变量的函数。当我们在闭包中查看this指向时,可以看到this指向了Window

var a = 10

var obj = {
  a: 20,
  foo: function() {
    var sum = this.count + 10
    
    console.log(this.a); // 20
    return function() {
      console.log(this.a); // 10
      return sum
    }
  }
}

obj.foo()()

上面代码中foo函数第一个this.athis指向为obj对象,因此结果为20,其返回函数调用的this指向Window,结果为10,obj.foo()()可以理解为:

const fn = obj.foo()
fn()

fnobj.foo()返回的一个函数,fn函数独立调用,this指向Window

隐式绑定

隐式绑定

当函数当作方法调用时,this指向函数的直接上级对象,称为隐式绑定。

在隐式绑定规则中认为,谁调用了函数,this就指向谁,而且会指向函数的直接上级对象。如obj.foo()中的foo函数内部的this指向obj, 而obj1.obj2.foo()中的foo函数this指向为obj2

var a = 10
function foo () {
  console.log(this.a);
}

var obj = {
  a: 20,
  foo: foo,
  obj2: {
    a: 30,
    foo: foo
  }
}

// exp1
foo() // 10

// exp2
obj.foo() // 20

// exp3 
obj.obj2.foo() // 30

上面代码中同样是foo函数的调用,调用方式不同,结果也不一样。

exp1中的foo为独立函数直接调用,因此this指向Window,结果为10;exp2中调用方法为obj.foo()foo函数的this指向上级调用对象为obj;结果为20,exp3中foo函数的直接上级对象是obj2, 因此结果为30。

隐式绑定丢失

隐式绑定丢失指被隐式绑定的函数丢失了绑定对象,从而默认绑定到Window。这种方法比较容易导致我们的项目出错,但也比较常见。

1. 隐式绑定的函数作为变量赋值,丢失this指向

下面代码中,obj下的foo值其实为foo函数的地址信息,并非真正的foo函数,obj.foo()调用时foothis隐式绑定到obj,而当var fn = obj.foo将函数赋值给fn后,相当于把foo函数的地址赋值给了fn,此时的fnobj并无关联,因此这里fn()运行时调用的运行环境是全局环境,this指向Windowthis.a结果为10。

var a = 10
var obj = {
  a: 20,
  foo: function () {
    console.log(this.a);
  }
}

obj.foo() // 20

var fn = obj.foo 
fn() // 10

2. 隐式绑定的函数作为参数传递给函数,丢失this指向
隐式绑定的函数,直接当作为参数传递给另一个函数时,会丢失this绑定,从而指向全局的Window。如下obj.foo作为参数传递给bar函数后,this.a结果为10。这里的bar(obj.foo)相当于var fn = obj.foo; bar(fn)

var a = 10
var obj = {
  a: 20,
  foo: function () {
    console.log(this.a);
  }
}
function bar (fn) {
  fn()
}

bar(obj.foo) // 10

3. 内置对象setTimeout、setInterval中函数隐式绑定丢失

内置函数setTimeoutsetIntervalthis默认指向Window

// exp1
setTimeout(function() {
  console.log(this); // Window
}, 1000)

// exp2
var a = 10
var obj = {
  a: 20,
  foo: function () {
    console.log(this.a); // 10
  }
}

setTimeout(obj.foo, 1000)

顺便提一下,当setTimeout或者setInterval的第一个参数为箭头函数时,this会指向上一层函数执行环境,代码如下:

var a = 10
var obj = {
  a: 20,
  foo: function () {
    console.log(this.a); // 20

    setTimeout(() => {
      console.log(this.a); // 20
    }, 1000)

    setTimeout(function() {
      console.log(this.a); // 10
    }, 1000);
  }
}

obj.foo()

显式绑定

当我们想要把函数绑定到指定对象上时,就可以使用callapplybind等方法手动改变this指向,即显式绑定。

下面代码中,分别用callapplybind三种方法例举了将foo显式绑定到p对象上的方法,其中callapply显式绑定的后会直接调用,而bind方法显式绑定this指向需要手动调用。

var a = 10
var obj = {
  a: 20,
  foo: function () {
    console.log(this.a);
  }
}
var p = {
  a: 30,
}

obj.foo() // 20
obj.foo.call(p) // 30
obj.foo.apply(p) // 30

const fn = obj.foo.bind(p)
fn() // 30

硬绑定

显式绑定可以帮助我们改变this指向到指定的对象,但是不能解决隐式绑定丢失的问题,如:

var a = 10
function foo() {
  console.log(this.a);
}

var obj = {
  a: 20,
  foo: foo
}

var p = {
  a: 30
}

function func(fn) {
  fn()
}

func.call(p, obj.foo) // 10

上面代码中使用call来绑定this指向到p对象上,但是最终this指向还是指向Window,此时我们可以通过硬绑定来解决这个问题。

var a = 10
function foo() {
  console.log(this.a);
}

var obj = {
  a: 20,
  foo: foo
}

var p = {
  a: 30
}

function func(fn) {
  fn()
}

let bar = function () {
  foo.call(p)
}
bar() // 30

new绑定

new绑定,是我们平时比较常用的,其实就是通过创建一个构造函数,然后new一个实例对象,此时的this指向就是new出来的实例对象。

当我们new一个对象的时候,主要做了以下几件事:

  1. 创建了一个新对象
  2. 让构造函数中的this指向新对象,并执行构造函数的函数体
  3. 设置新对象的proto属性指向构造函数的原型对象
  4. 判断构造函数的返回值类型,如果是值类型,则返回新对象。如果是引用类型,就返回这个引用类型的对象。

如下,先声明一个构造函数Person,通过new生成一个zhangsan的实例对象,则zhangsanfoo函数中,this的指向为zhangsan实例。

function Person(name, age) {
  this.name = name
  this.age = age
  this.foo = function () {
    console.log(this.name);
  }
}

const zhangsan = new Person('zhangsan', 18)
console.log(zhangsan) // {name: 'zhangsan', age: 18, foo: ƒ}
zhangsan.foo() // zhangsan

严格模式下this指向问题

1. 独立调用的函数内部thisundifined

function foo() {
  "use strict"
  console.log(this); undifined
}
foo()

2. call()apply()内部的this始终是它们的第一个参数

var a = 10
var obj = {
  a: 20,
  foo: function () {
    "use strict"
    console.log(this);
  }
}

// 为null | undefined时, 非严格模式下,this指向window
obj.foo.call(null) // null
obj.foo.call(undefined) // undefined
obj.foo.apply(null) // null
obj.foo.apply(undefined) // undefined
var fn = obj.foo.bind(null)
fn()

总结

this指向是个比较复杂的知识点,当然,如果我们真正理解了this指向的原理后,再遇到this指向的问题就变得很简单了,理解this指向,不仅是前端面试的加分项,也会对我们平时的开发和学习有诸多好处。

我们来总结一下。this指向的绑定原则有以下几种:

  • 默认绑定,this指向全局的Window
  • 隐式绑定,谁调用this就指向谁,别忘记隐式绑定丢失的情况
  • 显示绑定,通过使用callapplybind改变this指向
  • new绑定,对构造函数new一个实例,this指向new的实例对象

原文链接:https://www.jianshu.com/p/351d601cbed3

你可能感兴趣的:(JavaScript高级,javascript,前端)