这是一个前端面试常常考的根底考点,很多初学者在这个问题上都容易踩坑,包含我也是经常性蒙圈。所以这次决定将他们梳理下来,加深本人的了解。如果有出错的中央,欢送斧正。

this 是什么

this 关键字是 Javascript ES5 中最简单的机制之一。ES6 中新增的箭头函数,很大水平上防止了应用 this 所产生的谬误。然而在 ES5 中,有时候咱们会谬误的判断了 this 的指向。其实对于 this 的指向,始终保持一个原理:this 永远指向最初调用它的那个对象,记住了这句话,this 的指向你曾经理解一半了。

想要理解 this 的指向,咱们首先要理解 this 的四种绑定形式:隐式绑定、显示绑定、window 绑定、new 绑定

this 的四种绑定形式

隐式绑定

执行绑定的第一个也是最常见的规定为 隐式绑定,它 80% 的状况下会通知你 this 指向的对象是什么。

咱们先来看一个简略的例子:

  const user = {    name: 'Cherry',    age: 27,    getName() {      console.log(`Hello, my name is ${this.name}`)    }  }  user.getName()  // Hello, my name is Cherry

当咱们执行 user.getName() 时,会打印出Hello, my name is Cherry

如果你要调用 user 对象上的 getName 办法,你会用到点.

这就是所谓隐式绑定,函数被调用时先看一看点号左侧。如果有“点”就查看“点”左侧的对象,这个对象就是 this 的援用。

在下面的例子中,user 在“点号左侧”意味着 this 援用了 user 对象。所以就如同 在 getName 办法的外部 JavaScript 解释器把 this 变成了 user

所以,你能够得出这样的论断:应用对象来调用其外部的一个办法,该办法的 this 是指向对象自身的。这就是所谓隐式绑定,你也能够这样认为:JavaScript解释器在执行 user.getName()时,将其转化为了:

user.getName.call(user);

咱们将代码减少一层调用:

const user = {  name: 'Cherry',  age: 27,  getName() {    console.log(`Hello, my name is ${this.name}`)  },  mother: {    name: 'Susan',    getName() {      console.log(`Hello, my name is ${this.name}`)    }  }}user.getName()  // Hello, my name is Cherryuser.mother.getName()    // Hello, my name is Susan

正如方才所说:this 永远指向最初调用它的那个对象,那么“点”左侧的对象即为后调用该办法的对象,this 指向该对象。然而,如果没有点呢?这就为咱们引出了下一条规定:

显示绑定

对于显示绑定,咱们能够通过 call 来设置函数执行上下文的 this 指向,比方上面这段代码:

function getName () {  console.log(`Hello, my name is ${this.myName}`)}let user = {  myName: 'Cherry',  age: 27,}getName.call(user)  // Hello, my name is Cherry

执行这段代码,而后察看输入后果,你会发现 getName 函数外部的 this 曾经指向了 user 对象。

其实除了 call 办法,咱们还能够应用 bind 和 apply 办法来设置函数执行上下文中的 this,它们在应用上有一些区别,文章的第六大节会对 call、apply、bind 进行具体的介绍,这里我就不过多赘述了。

window 绑定

咱们在方才的例子的根底上批改一下:

function getName () {  console.log(`Hello, my name is ${this.myName}`)}let user = {  myName: 'Cherry',  age: 27,}getName();

置信大家都晓得为什么打印进去的是 My name is undefined,因为正如后面所说的,如果你想用 user 做上下文调用 getName,你能够应用 .call.apply.bind。但如果咱们没有用这些办法,而是间接和平时一样间接调用,JavaScript 会默认 this 指向 window 对象。然而 window 对象中并没有 myName 属性,所以会打印 “My name is undefined“

在 ES5 增加的 严格模式 中,JavaScript 不会默认 this 指向 window 对象,而会正确地把 this 放弃为 undefined。

例如:

'use strict'age = 27function sayAge () {  console.log(`Hello, my age is ${this.age}`)}sayAge() // TypeError: Cannot read property 'age' of undefined

new 绑定

第四条判断 this 援用的规定是 new 绑定。每当用 new 调用函数时,JavaScript 解释器都会在底层创立一个全新的对象并把这个对象当做 this

这看起来就像创立了新的函数,但实际上 JavaScript 函数是从新创立的对象。

例如:

function User (name, age) {  /*    JavaScript 会在底层创立一个新对象 `this`,它会代理不在 User 原型链上的属性。    如果一个函数用 new 关键字调用,this 就会指向解释器创立的新对象。  */  this.name = name  this.age = age}const me = new User('Cherry', 27)

伪代码示意:

var me = new User("Cherry","27");new User{  var object = {};  object.__proto__ = User.prototype;  var result = User.call(object,"Cherry","27");  return typeof result === 'object'? result : object;}

new 的过程:

  1. 创立一个空对象 object;
  2. 将新创建的空对象的隐式原型指向其构造函数的显示原型;
  3. 应用 call 扭转 this 的指向;
  4. 如果无返回值或者返回一个非对象值,则将 object 返回作为新对象;如果返回值是一个新对象的话那么间接间接返回该对象。

所以咱们能够看到,在 new 的过程中,其实是应用 call 扭转了 this 的指向。

this 的指向

后面讲了对于 this 的四种绑定形式,咱们对于 this 的指向应该也有了一些本人的了解,还记得咱们之前说的吗?this 永远指向最初调用它的那个对象,咱们记好这句话来练习上面的例子:

练1:

var name = "window";function fn() {  var name = "Cherry";  console.log(this.name);          // window  console.log("inner:" + this);    // inner: Window}fn();console.log("outer:" + this)         // outer: Window

咱们看最初调用 fn 的中央 fn();,后面没有“点”,Javascript 调用的对象默认指向了全局对象 window,这就相当于是 window.fn();所以依据刚刚的那句话“this 永远指向最初调用它的那个对象”,this 指向的就是 window。绑定规定是Window绑定。

留神,这里咱们没有应用严格模式,如果应用严格模式的话,全局对象就是 undefined,那么就会报错 Uncaught TypeError: Cannot read property 'name' of undefined。

练2:

var name = "window";var user = {  name: "Cherry",  fn: function () {      console.log(this.name);      // Cherry  }}user.fn();

依据上文所说,咱们看到函数 fn 左侧有“点”,“点”的左侧是 user,所以 fn 是对象 user 调用的。所以打印的值就是 user 中的 name 的值。绑定规定是隐式绑定。

练3:

var name = "window";function fnA(){  var name = "Cherry";  function fnB(){    console.log(this.name);    // window   }  //在A函数外部调用B函数  fnB();}//调用A函数fnA();

嵌套函数中的 this 不会从外层函数中继承。在函数执行环境中应用 this 时,如果函数没有显著的作为非 window 对象的属性,而只是定义了函数,这个函数中的 this 依然默认指向 window 对象。

练4:

var name = "window";var user = {  name: "Cherry",  fn: function () {    console.log(this.name);      // Cherry  }}window.user.fn();

这里打印 Cherry 的起因也是因为刚刚那句话“this 永远指向最初调用它的那个对象”,最初调用它的对象依然是对象 user。

咱们改变一下:

var name = "window";var user = {  // name: "Cherry",  fn: function () {    console.log(this.name);      // undefined  }}window.user.fn();

这是因为调用 fn 的是 user 对象,也就是说 fn 的外部的 this 是对象 user,而对象 user 中并没有对 name 进行定义,所以 logthis.name 的值是 undefined

这个例子还是阐明了:this 永远指向最初调用它的那个对象,因为最初调用 fn 的对象是 user,所以就算 user 中没有 name 这个属性,也不会持续向上一个对象寻找 this.name,而是间接输入 undefined

练5:(这个例子稍稍有点坑)

var name = "window";var user = {  name : null,  // name: "Cherry",  fn : function () {    console.log(this.name);      // window  }}var f = user.fn;f();

这里你可能会有疑难,为什么不是 Cherry?因为这里尽管将 user 对象的 fn 办法赋值给变量 f 了,然而没有调用,再接着跟我念这一句话:“this 永远指向最初调用它的那个对象”,因为刚刚的 f 并没有调用,所以 fn() 最初依然是被 window 调用的。所以 this 指向的也就是 window

由以上五个练习咱们能够看出,this 的指向并不是在创立的时候就能够确定的,在 es5 中,this永远指向最初调用它的那个对象。

如何扭转 this 的指向

扭转 this 的指向我总结有以下几种办法:

  • 应用 ES6 的箭头函数
  • 在函数外部应用 _this = this
  • 应用 apply、call、bind
  • new 实例化一个对象

咱们看上面的例子:

var name = "window";var user = {  name : "Cherry",  fn1: function() {    console.log(this.name)       },  fn2: function() {    setTimeout(function () {      this.fn1()    },100);  }};user.fn2()     // this.fn1 is not a function

咱们逐个细说一下这个例子:fn2()是被 user调用的,所以fn2中的 this 应该指向 user。然而fn2中又调用了 window 中的 setTimeout 办法。所以在 setTimeout 办法中的 this 指向的是后调用它的对象 window。然而在 window 中并没有 fn1 函数。所以抛出谬误:this.fn1 is not a function。

如果咱们想正确的调用 user 中的 fn1(),应该怎么做呢?咱们把这个例子作为 demo 进行革新。

箭头函数

家喻户晓,ES6 的箭头函数是能够防止 ES5 中应用 this 的坑的。“所有的箭头函数都没有本人的this,都指向外层。”--这句话就是箭头函数的精华。箭头函数的this,总是指向定义时所在的对象,而不是运行时所在的对象。这句话说的太含糊了,最好改成:总是指向所在函数运行时的this

下面例子咱们应用箭头函数扭转this的指向如下:

var name = "window";var user = {  name : "Cherry",  fn1: function () {    console.log(this.name)       },  fn2: function () {    setTimeout( () => {        this.fn1()    },100);  }};user.fn2()     // Cherry

对于箭头函数,咱们还须要留神以下几点:

  1. 函数体内的this就是定义时所在的对象,而非调用时所在的对象,和一般函数相同。
  2. 箭头函数无奈用做构造函数,即不能应用new调用
  3. 不能应用arguments对象,函数中不存在这个对象。
  4. 不可应用yield命令,即无奈用做Generator函数。

其中第一点尤其值得注意,之所以this是固定的,是因为箭头函数自身没有this,箭头函数的this不是本人的。所以不能批改,也正因为没有this,所以不能用作构造函数。这些限度都是因为没有this导致的。

在函数外部应用 _this = this

如果不应用 ES6,那么这种形式应该是最简略的不会出错的形式了,咱们是先将调用这个函数的对象保留在变量 _this 中,而后在函数中都应用这个 _this,这样 _this 就不会扭转了。

var name = "window";var user = {  name : "Cherry",  fn1: function () {    console.log(this.name)       },  fn2: function () {    var _this = this;    setTimeout( function() {      _this.fn1()    },100);  }};user.fn2()       // Cherry

这个例子中,在 fn2 中,首先设置 var _this = this;,这里的 this 是调用 fn2 的对象 user,为了避免在 fn2 中的 setTimeoutwindow 调用而导致的在 setTimeout 中的 thiswindow。咱们将 this(指向变量 user) 赋值给一个变量 _this,这样,在 fn2 中咱们应用 _this 就是指向对象 user 了。

应用 apply、call、bind

应用 apply、call、bind 函数也是能够扭转 this 的指向的,成为显示绑定,咱们先来看一下是怎么实现的:

应用 apply()

var user = {  name: "Cherry",  fn1: function() {    console.log(this.name)  },  fn2: function() {    setTimeout(function () {      this.fn1()    }.apply(user), 100);  }};user.fn2()            // Cherry

应用 call()

var user = {  name: "Cherry",  fn1: function() {    console.log(this.name)  },  fn2: function() {    setTimeout(function () {      this.fn1()    }.call(user), 100);  }};user.fn2()            // Cherry

应用 bind()

var user = {  name: "Cherry",  fn1: function() {    console.log(this.name)  },  fn2: function() {    setTimeout(function () {      this.fn1()    }.bind(user)(), 100);  }};user.fn2()            // Cherry

apply、call、bind 的区别

刚刚咱们曾经介绍了 apply、call、bind 都是能够扭转 this 的指向的,然而这三个函数稍有不同。

在 MDN 中定义 apply 如下;

apply() 办法调用一个函数, 其具备一个指定的this值,以及作为一个数组(或相似数组的对象)提供的参数

apply 和 call 的区别

其实 apply 和 call 根本相似,他们的区别只是传入的参数不同。

call 的语法为:

fun.call(thisArg[, arg1[, arg2[, ...]]])

所以 apply 和 call 的区别是 call 办法承受的是若干个参数列表,而 apply 接管的是一个蕴含多个参数的数组。

apply()的应用办法:

var user ={  name: "Cherry",  fn: function(a,b) {    console.log(a + b)  }}var newUser = user.fn;newUser.apply(user,[1,2])     // 3

call()的应用办法:

var user ={  name: "Cherry",  fn: function(a,b) {    console.log(a + b)  }}var newUser =user.fn;newUser.call(user, 1, 2)       // 3

但凡事都有例外:
若将null、undefined等值作为call、apply的第一个参数,那么理论调用时会被疏忽,从而利用到Window绑定规定,即绑定到window上,有些时候咱们不关怀上下文,只关怀参数时,能够这样做。

但这样其实存在这一些潜在的危险,绑定到window很可能无心中增加或批改了全局变量,造成一些荫蔽的bug。所以为了避免这种状况呈现,能够将第一个参数绑定为一个空对象。当然具体还是看需要,这只是倡议。

bind 和 apply、call 区别

咱们先应用 bind 试一下刚刚的例子:

var user ={  name: "Cherry",  fn: function(a,b) {    console.log(a + b)  }}var newUser = user.fn;nreUser.bind(user,1,2)

咱们会发现并没有输入,这是为什么呢,咱们来看一下 MDN 上的文档阐明:

bind()办法创立一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

所以咱们能够看出,bind 是创立一个新的函数,咱们必须要手动去调用:

var user ={  name: "Cherry",  fn: function (a,b) {    console.log( a + b)  }}var newUser = user.fn;newUser.bind(user,1,2)()           // 3

以上就是三种显示绑定的办法,但有三点须要留神:

  1. call和apply是立刻执行,bind则是返回一个绑定了this的新函数,只有你调用了这个新函数才真的调用了指标函数
  2. bind函数存在屡次绑定的问题,如果屡次绑定this,则以第一次为准。
  3. bind函数实际上是显示绑定(call、apply)的一个变种,称为硬绑定。因为硬绑定是一种十分罕用的模式,所以在 ES5 中提供了内置的办法Function.prototype.bind

为什么屡次应用bind绑定this,以第一次为准呢?咱们看上面的例子:

function foo() {  console.log( this.name );} var obj1 = {  name: 'obj1'}; var obj2 = {  name: 'obj2'}var fn = foo.bind(obj1).bind(obj2)fn() // => 'obj1'fn.call(obj2) // => 'obj1'

也就是说bind函数只能绑定一次,屡次绑定是没有用的,绑定后的函数this无奈扭转,即便call/apply也不行,所以才称作硬绑定。

但凡事总有例外,且看new绑定。

绑定的优先级

如果显示绑定和new绑定同时存在,或者更宽泛的说:在某个调用地位多条绑定规定同时存在怎么办呢?为了解决这个问题就必须给这些规定设定优先级,这就是咱们接下来要介绍的内容。

毫无疑问,Window绑定的优先级是最低的,显式绑定和隐式绑定的优先级,通过下面的例子也能够证实,显式大于隐式。所以目前程序是:显式 > 隐式 > Window

那咱们来测试下显示绑定和new绑定的优先级程序。因为call/apply无奈和new一起应用,咱们能够应用bind(硬绑定)来验证。

function foo() {  this.name = 'Cherry';} var obj = {  name: 'obj'}; var fn = foo.bind(obj)var result = new fn()console.log(obj.name) // => 'obj'console.log(result.name) // => 'Cherry'

不言而喻的,new的优先级,大于显示绑定。最终程序为:new > 显式 > 隐式 > Window

于是咱们判断this,就有了一个程序:

  1. 函数是否在new中调用?
  2. 是否通过call、apply、bind等调用?
  3. 是否在某个上下文对象中调用?
  4. 都不是则是Window绑定。且严格模式下绑定到undefined。

小结

  • this 的四种绑定形式:隐式绑定、显示绑定、window 绑定、new 绑定
  • 扭转 this 的指向有以下几种办法:

    1. 箭头函数
    2. 在函数外部应用 _this = this
    3. 应用 apply、call、bind
    4. 应用 new
  • 判断this次要有以下步骤:

    1. 函数是否在new中调用?
    2. 是否通过call、apply、bind等调用?
    3. 是否在某个上下文对象中调用?
    4. 都不是则是默认绑定。且严格模式下绑定到undefined。
  • 绑定优先级:new > 显式 > 隐式 > Window

另外还要留神箭头函数的特殊性、在call/apply中应用undefined和null会被疏忽这一个性、bind的硬绑定以及:this永远指向最初调用它的那个对象。
以上就是对于判断this指向的总结,理解了以上几个关键点,this的指向你曾经很理解啦~

对于

作者齐小神,前端程序媛一枚。

有点文艺,喜爱摄影。 尽管当初朝九晚五,埋头苦学, 但幻想是做女侠,扶贫济穷,仗剑走咫尺。 心愿有一天能改完 BUG 去实现本人的幻想。

公众号:大前端Space,不定时更新,欢送来玩~