最全汇总🔥一文彻底搞懂this指向

795 阅读24分钟

this到底指向谁?

相信很多小伙伴曾几何时都被这个问题问倒过吧? 特别是对于初学者而言,this的指向问题确实是一个令人头疼的问题。而对于要参加面试的同学来说,this的指向无疑是一个必考题

之前我也是对this的指向感觉很迷惑,于是就看了好几篇有关this指向的优秀文章,最后也算是有了一个较为清晰的认识,现在就将其总结一下吧!

在开始之前,建议可以复习一下作用域的相关知识,可以看一下我之前的一篇文章:详解JavaScript作用域和作用域链🔗

OK,接下来就开始吧!

📍判断this的基本流程

首先,先给出我对于this指向判断的一个基本流程:

1、普通函数的this

  • 通过new操作符绑定(创建一个实例对象,构造函数中的this指向这个实例对象)
  • 通过显式绑定(通过callapplybind这三个函数指定this的绑定对象)
  • 通过隐式绑定(指向最后调用它的那个对象,如 obj1.obj2.foo()的调用方式, foo内的this指向obj2
  • 默认绑定(非严格模式下this指向全局对象, 严格模式下this会绑定到undefined

2、箭头函数的this

  • 指向定义该函数时的上一级作用域

接下来,我们就对上述每个步骤进行详细描述。

🖐普通函数的this

1、通过new绑定

当我们使用new操作符来调用构造函数时,会自动执行下面的操作:

  1. 创建一个空对象,构造函数中的this指向这个空对象
  2. 这个空对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
  3. 执行构造函数方法,属性和方法被添加到this引用的对象中
  4. 如果构造函数中没有返回其它对象,那么返回this,即创建的这个的新对象,否则,返回构造函数中返回的对象。

关于new一个构造函数的过程,又是一个高频面试题,但不是本文的主要内容,所以不作过多描述。接下来,我们看一个例子:

function SayHi (name) { 
    this.name = name
} 
var Hi = new SayHi('Rocky')
console.log('Hello,', Hi.name)

输出结果为 Hello, Rocky

因为在var Hi = new SayHi('Rocky')这一步,会将SayHi中的this绑定到Hi对象上。

此外,使用new操作符创建的对象和字面量形式创建出来的对象的结果基本没什么区别,看一下下面这个例子:

var name = 'window'
function Person (name) {
  this.name = name
  this.foo = function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var person2 = {
  name: 'person2',
  foo: function() {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
  
var person1 = new Person('person1')
person1.foo()()
person2.foo()()

首先,person1.foo()会打印this.name,这里的this肯定是指向person1的,所以会打印person1,然后它会返回一个函数并且在全局下调用了:person1.foo()(),所以输出的是全局下的name,所以为windowperson2的情况跟person1是一样的,因为foo()是被person2调用的,属于隐式绑定(下文会解释),所以this是指向person2的。

所以,输出结果为:

'person1'
'window'
'person2'
'window'

new过程遇到return一个对象,此时this指向为返回的对象

function fn() {  
    this.user = 'xxx';  
    return {};  
}
var a = new fn();  
console.log(a.user); //undefined

如果返回一个简单类型的时候,则this指向实例对象

function fn()  
{  
    this.user = 'xxx';  
    return 1;
}
var a = new fn;  
console.log(a.user); //xxx

注意的是null虽然也是对象,但是此时new仍然指向实例对象

function fn() {  
    this.user = 'xxx';  
    return null;
}
var a = new fn;  
console.log(a.user); //xxx

2、默认绑定

由于后面 显式绑定 需要用到 默认绑定隐式绑定 的相关知识,所以这里把顺序颠倒一下。

默认绑定,是在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。

对于默认绑定,只需记住:非严格模式下this指向全局对象, 严格模式下this会绑定到undefined。 下面看一个例子:

function sayHi () { 
    console.log('Hello,', this.name)
} 
var name = 'Rocky'
sayHi() 

输出:Hello,Rocky

在调用sayHi()时,应用了默认绑定,this指向全局对象(非严格模式下),严格模式下,this指向undefinedundefined上没有this对象,会抛出错误。

因为这里的sayHi()是在全局环境中调用的,所以也可以看成window.sayHi(),两者是等价的,这样的话this的指向就很直观了,指向的是window

上面的代码,如果在浏览器环境中运行,那么结果就是 Hello,Rocky

但是如果在node环境中运行,结果就是Hello,undefined,这是因为nodename并不是挂在全局对象上的。

本文中,如不特殊说明,默认为浏览器环境执行结果。

再看一个例子:

var name = "windowsName"

function fn() {
    var name = 'Cherry'
    innerFunction()
    function innerFunction() {
        console.log(this.name)      // windowsName
    }
}

fn()

这里的innerFunction作为一个没有挂载在任何对象上的函数调用,使用的是默认绑定,在非严格模式下 this 就是指向 window

注意,如果变量不是用var声明的,而是用letconst声明的,那么它不会被绑定到window对象上:

let a = 10
const b = 20

function foo () {
  console.log(this.a)
  console.log(this.b)
}
foo()
console.log(window.a)

输出结果为

undefined 
undefined 
undefined

3、隐式绑定

函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的形式为 XXX.fun()。我们来看一段代码:

function sayHi(){
    console.log('Hello,', this.name)
}
var person = {
    name: 'Rocky',
    sayHi: sayHi
}
var name = 'will'
person.sayHi()

打印的结果是 Hello,Rocky

sayHi函数声明在外部,严格来说并不属于person,但是在调用sayHi时,调用位置会使用person的上下文来引用函数,隐式绑定会把函数调用中的this(即此例sayHi函数中的this)绑定到这个上下文对象(即此例中的person

需要注意的是:对象属性链中只有最后一层会影响到调用位置。来看下面的例子:

function sayHi(){
    console.log('Hello,', this.name)
}
var person2 = {
    name: 'Rocky',
    sayHi: sayHi
}
var person1 = {
    name: 'will',
    friend: person2
}
person1.friend.sayHi()

结果是:Hello, Rocky

因为只有最后一层会确定this指向的是什么,不管有多少层,在判断this的时候,我们只关注最后一层,即此处的friend

隐式绑定有一种情况,就是发生隐式绑定丢失情况。隐式丢失其实就是被隐式绑定的函数在特定的情况下会丢失绑定对象。

有两种情况容易发生隐式丢失问题:

  • 使用另一个变量来给函数取别名
  • 将函数作为参数传递时会被隐式赋值,回调函数丢失this绑定

接下来,我们来看一下具体的例子:

function sayHi(){
    console.log('Hello,', this.name)
}
var person = {
    name: 'Rocky',
    sayHi: sayHi
}
var name = 'will'
var Hi = person.sayHi
Hi()

结果是: Hello,will

因为person直接把sayHi方法赋给了Hi,但是没有调用。由于this指向最后调用它的那个对象,最后Hi是在全局window下调用的,所以sayHi最终还是在window下调用的。也就是说,sayHiperson就没有任何半毛钱的关系。 this 指向的也就是window

针对此类问题,我们只需牢牢记住这个格式:XXX.fn()fn()前如果什么都没有,那么肯定不是隐式绑定(或隐式绑定丢失)。上面的代码中,Hi()前面没有任何对象去调用它,所以就不是隐式绑定(或隐式绑定丢失)

如果你把一个函数当成参数传递时,也会发生隐式绑定丢失

function foo () {
  console.log(this.a)
}
function doFoo (fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)  

输出为:window 2

这里将obj.foo当成参数传递到doFoo函数中,在传递的过程中,obj.foo()函数内的this发生了改变,指向了window。所以输出的a是全局下的a。

这里有一个坑,就是很多人以为obj.foo()函数内的this指向window,是因为它是在doFoo里面调用的,doFoothis就是window,但其实不是这样的,我们再来看一题:

现在我们不用window调用doFoo,而是放在对象obj2里,用obj2调用

function foo () {
  console.log(this.a)
}
function doFoo (fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
var obj2 = { a: 3, doFoo }

obj2.doFoo(obj.foo)

结果是:

{ a:3, doFoo: f }
2

现在调用obj2.doFoo()函数,里面的this指向的应该是obj2,因为是obj2调用的它。

但是obj.foo()打印出来的a依然是2,也就是window下的a。

对于这个问题,只需记住以下这个结论即可:

如果把一个函数当成参数传递到另一个函数的时候,也会发生隐式丢失的问题,且与包裹着它的函数的this指向无关(比如上面的doFoo()this)。在非严格模式下,会把该函数的this绑定到window上,严格模式下绑定到undefined。

下面再来看一个不同的例子:

function foo() {
  setTimeout(function () {
    console.log('id:', this.id)
  }, 100);
}

var id = 21
foo() // 21

在上面的例子中,function () { console.log('id:', this.id) }这个匿名函数被当成参数传递给setTimeout,发生了隐式绑定丢失,所以this应该指向全局对象window,所以输出的是全局的变量id。我们也可以记住一个结论:匿名函数的this永远指向window

接下来我们看一个综合一点的小例子:

function sayHi(){
    console.log('Hello,', this.name)
}
var person1 = {
    name: 'Rocky',
    sayHi: function(){
        setTimeout(function(){
            console.log('Hello',this.name)
        })
    }
}
var person2 = {
    name: 'will',
    sayHi: sayHi
}
var name='skumion'
person1.sayHi()
setTimeout(person2.sayHi,100)
setTimeout(function(){
    person2.sayHi()
},200)

结果为:

Hello, skumion 
Hello, skumion 
Hello, will
  • 第一条输出很容易理解,刚刚才讲过,是隐式绑定丢失的情况,this执指向window

  • 第二条输出也是隐式绑定丢失的情况, setTimeout (fn,delay) { fn() }相当于是将person2.sayHi赋值给了一个变量fn,最后执行了变量,这个时候,sayHi中的this显然和person2就没有关系了。

  • 第三条虽然也是在setTimeout的回调中,但是我们可以看出,这是执行的是person2.sayHi()使用的是隐式绑定,因此这是this指向的是person2,跟当前的作用域没有任何关系。

4、显式绑定

显式绑定就是通过call,apply,bind的方式,显式的指定this所指向的对象。

call,applybind的第一个参数,就是对应函数的this所指向的对象。callapply的作用一样,只是传参方式不同。call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组

  • call的语法:function.call(thisArg, arg1, arg2, ...)
  • apply的语法:function.apply(thisArg , [ argsArray])
  • bind的语法:function.bind(thisArg, arg1, arg2, ...)

call()apply()不一样的是,bind()方法创建一个新的函数,必须手动调用它才会被执行。

var name = 'Rocky', age = 23
var obj = {
    name: 'will',
    objAge: this.age,
    myFun: function () {
        console.log(this.name + '年龄' + this.age)
    }
}
var anotherObj = {
    name: 'skumion',
    age: 18
}

输出结果如下

obj.myFun.call(anotherObj)    // skumion年龄 18
obj.myFun.apply(anotherObj)    // skumion年龄 18
obj.myFun.bind(anotherObj)()    // skumion年龄 18

再来看一个例子

function sayHi(){
    console.log('Hello,', this.name)
}
var person = {
    name: 'Rocky',
    sayHi: sayHi
}
var name = 'will'
var Hi = person.sayHi
Hi.call(person) // 等于Hi.apply(person)

输出的结果为: Hello, Rocky

因为使用显式绑定明确将this绑定在了person上。

如果通过callapplybindthis绑定到nullundefined身上时,会怎么样呢?

var foo = {
    name: 'will'
}
var name = 'Rocky'
function bar() {
    console.log(this.name)
}
bar.call(null); //Rocky 

从上例中可以看出,this绑定到nullundefined身上时会被忽略,实际应用的是默认绑定规则。

此外,使用了显式绑定,也会出现隐式绑定所遇到的绑定丢失的情况。

function sayHi(){
    console.log('Hello,', this.name)
}
var person = {
    name: 'Rocky',
    sayHi: sayHi
}
var name = 'will'
var Hi = function(fn) {
    fn()
}
Hi.call(person, person.sayHi)

输出的结果是 Hello, will

原因很简单,Hi.call(person, person.sayHi)的确是将this指向person了。但是在执行fn的时候,相当于直接调用了sayHi方法(记住: person.sayHi已经被赋值给fn了,隐式绑定也丢了),没有指定this的值,对应的是默认绑定。

如果我们希望绑定不会丢失,要怎么做?很简单,调用fn的时候,再给它显式绑定。

function sayHi(){
    console.log('Hello,', this.name)
}
var person = {
    name: 'Rocky',
    sayHi: sayHi
}
var name = 'will'
var Hi = function(fn) {
    fn.call(this)
}
Hi.call(person, person.sayHi)

此时,输出的结果为: Hello, Rocky

因为person被绑定到Hi函数中的this上,fn又使用了call(this)将这个对象绑定给了sayHi的函数。这时,sayHi中的this指向的就是person对象。

OK,再来看一个例子:

var obj1 = {
  a: 1
}
var obj2 = {
  a: 2,
  foo1: function () {
    console.log(this.a)
  },
  foo2: function () {
    setTimeout(function () {
      console.log(this)
      console.log(this.a)
    }.call(obj1), 0)
  }
}
var a = 3
obj2.foo1()
obj2.foo2()

结果是:

2
{ a: 1 }
1

相信这题对于现在的大家来说根本没有难度,但是使用这个例子是为了引出一个细节问题:

既然call能改变this指向,那我这样写也行吧? obj2.foo2.call(obj1)

注意:这是错误的!如果是这种写法的话,改变的就是foo2函数内的this的指向了,但是我们知道,foo2函数内this的指向和setTimeout里函数的this是没有关系的,因为调用定时器的始终是window

接下来再来一个容易被忽视的细节问题:

function foo () {
  console.log(this.a)
}
var obj = { a: 1 }
var a = 2

foo()
foo.call(obj)
foo().call(obj)
  • foo()会正常打印出window下的a,也就是2
  • foo.call(obj)由于显式绑定了this,所以会打印出obj下的a,也就是1
  • foo().call(obj)开始会执行foo()函数,打印出2,但是会对foo()函数的返回值执行.call(obj)操作,可是我们可以看到foo()函数的返回值是undefined,因此就会报错了。

输出结果为:

2
1
2
Uncaught TypeError: Cannot read property 'call' of undefined

所以我们可以看到foo.call()foo().call()的区别了,一个是针对于函数,一个是针对于函数的返回值。

刚刚是因为函数没有返回值才报的错,那现在给它加上返回值看看:

function foo () {
  console.log(this.a)
  return function () {
    console.log(this.a)
  }
}
var obj = { a: 1 }
var a = 2

foo()
foo.call(obj)
foo().call(obj)
  • 第一个数字2自然是foo()输出的,虽然foo()函数也返回了一个匿名函数,但是并没有调用它呀,只有写成foo()(),这样才算是调用匿名函数。
  • 第二个数字1foo.call(obj)输出的,由于.call()是紧跟着foo的,所以改变的是foo()this的指向,并且.call()是会使函数立即执行的,因此打印出1,同理,它也没有调用返回的函数。
  • 第三个数字2foo().call(obj)先执行foo()时打印出来的,此时foo()this还是指向window
  • 在执行完foo()之后,会返回一个匿名函数,并且后面使用了.call(obj)来改变这个匿名函数的this指向并调用了它,所以输出了1

输出结果为:

2
1
2
1

5、优先级

到此,已经介绍完了this的四种绑定规则,如果同时应用了多种规则,这四种绑定的优先级为:

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

🖖箭头函数的this

箭头函数有几个使用注意点:

  • 箭头函数没有自己的this对象(详见下文),所以不能用call()、apply()、bind()这些方法去改变this的指向.
  • 不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

上面四点中,最重要的是第一点。对于普通函数来说,内部的this指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this对象,内部的this就是定义时上层作用域中的this。也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的。

1、this指向

function foo() {
  setTimeout(() => {
    // 定义时,this 绑定的是 foo 中的 this 对象
    console.log('id:', this.id)
  }, 100);
}

var id = 21
foo.call({ id: 42 })  // id: 42

上面代码中,setTimeout()的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以打印出来的是42

下面例子是回调函数分别为箭头函数和普通函数,对比它们内部的this指向。

function Timer() {
  this.s1 = 0
  this.s2 = 0
  // 箭头函数
  setInterval(() => this.s1++, 1000)
  // 普通函数
  setInterval(function () {
    this.s2++
  }, 1000)
}

var timer = new Timer()
setTimeout(() => console.log('s1: ', timer.s1), 3100)
setTimeout(() => console.log('s2: ', timer.s2), 3100)
// s1: 3
// s2: 0

上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。

箭头函数实际上可以让this指向固定化,绑定this使得它不再可变,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。

var handler = {
  id: '123456',
  init: function() {
    document.addEventListener('click',
      event => this.doSomething(event.type), false)
  },
  doSomething: function(type) {
    console.log('Handling ' + type  + ' for ' + this.id)
  }
}

上面代码的init()方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。如果回调函数是普通函数,那么运行this.doSomething()这一行会报错,因为此时this指向window对象(匿名函数的this指向window)。

下面是 Babel 转箭头函数产生的 ES5 代码,就能清楚地说明this的指向。

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id)
  }, 100);
}

// ES5
function foo() {
  var _this = this
  setTimeout(function () {
    console.log('id:', _this.id)
  }, 100)
}

上面代码中,转换后的 ES5 版本清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this

另外,由于箭头函数没有自己的this,所以当然也就不能用call()apply()bind()这些方法去改变this的指向。

(function() {
  return [
    (() => this.x).bind({ x: 'inner' })()
  ]
}).call({ x: 'outer' })
// ['outer']

上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this

2、不适用场合

由于箭头函数使得this从“动态”变成“静态”,下面四个场合不应该使用箭头函数。

第一个场合是定义对象的方法,且该方法内部包括this

const cat = {
  lives: 9,
  jumps: () => {
    this.lives--
  }
}

上面代码中,cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。

globalThis.s = 21;

const obj = {
  s: 42,
  m: () => console.log(this.s)
};

obj.m() // 21

上面例子中,obj.m()使用箭头函数定义。JavaScript 引擎的处理方法是,先在全局空间生成这个箭头函数,然后赋值给obj.m,这导致箭头函数内部的this指向全局对象,所以obj.m()输出的是全局空间的21,而不是对象内部的42。上面的代码实际上等同于下面的代码。

globalThis.s = 21;
globalThis.m = () => console.log(this.s);

const obj = {
  s: 42,
  m: globalThis.m
};

obj.m() // 21

由于上面这个原因,对象的属性建议使用传统的写法定义,不要用箭头函数定义。

第二个场合是需要动态this的时候,也不应使用箭头函数。

var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});

上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

第三个场合是定义原型方法时,也不应该使用箭头函数

function Foo (value) {
    this.value = value
}
Foo.prototype.getValue = () => console.log(this.value)

const foo1 = new Foo(1)
foo1.getValue()  // undefined

第四个场合是构造函数也不应该使用箭头函数

const Foo = (value) => {
    this.value = value;
}
const foo1 = new Foo(1)
// 事实上直接就报错了 Uncaught TypeError: Foo is not a constructor
console.log(foo1)

3、Vue中的this

所有被Vue管理的函数,最好写成普通函数,这样this的指向才是vm组件实例对象。如:

computed: {
    foo () {
        console(this) // vm
    }
},
methods: {
    bar () {
        console(this) // vm
    }
}

所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数等、Promise的回调函数等),最好写成箭头函数,这样this的指向才是vm组件实例对象 如:

methods: {
    foo () {
        setTimeOut (() => {
            console.log(this) // vm
        }, 1000)
    },
    bar () {
        return new Promise ((resolve, reject) => {
            resolve()
            console.log(this) // vm
        })
    }
}

📒练习题

第一题

"use strict"
var a = 10
function foo () {
  console.log('this1', this)
  console.log(window.a)
  console.log(this.a)
}
console.log(window.foo)
console.log('this2', this)
foo()
  • 开启了严格模式,只是说使得函数内this指向undefined,它并不会改变全局中this的指向。因此this1中打印的是undefined,而this2还是window对象。
  • 另外,它也不会阻止a被绑定到window对象上。

输出结果为:

f foo() {...}
'this2' Window{...}
'this1' undefined
10
Uncaught TypeError: Cannot read property 'a' of undefined

第二题

var name = "windowsName"
var a = {
    name : "Rocky",
    func1: function () {
        console.log(this.name)     
    },
    func2: function () {
        setTimeout( function () {
            this.func1() 
        },100)
    }
};
a.func2()

匿名函数function () { this.func1() }被当成参数传递给setTimeout,发生了隐式绑定丢失,所以this指向全局window对象,但是在 window 中并没有 func1 函数。

所以输出结果会报错:this.func1 is not a function

第三题

function foo () {
  console.log(this.a)
  return function () {
    console.log(this.a)
  }
}
var obj = { a: 1 }
var a = 2

foo.call(obj)()

首先foo.call(obj)打印obj里的1,然后foo又返回了一个匿名函数。foo()函数内的this虽然指定了是为obj,但是调用最后调用匿名函数的却是window。因为foo.call(obj)()是在全局下调用的。

输出结果为:

1
2

第四题

function foo () {
  console.log(this.a)
}
var obj = { a: 1, foo };
var a = 2
var foo2 = obj.foo
var obj2 = { a: 3, foo2: obj.foo }

obj.foo()
foo2()
obj2.foo2()

obj.foo()中,foo是被obj调用的,所以foo里面的this指向obj,输出1

var foo2 = obj.foo是隐式绑定丢失的情况,所以 foo2()是直接在window下调用了foo(),所以输出2

var obj2 = { a: 3, foo2: obj.foo }同样也是隐式绑定丢失情况,obj.foo直接赋给了foo2,然后foo2obj2调用,所以输出3

所以输出结果为:

1
2
3

第五题

请问下面的代码之中,this的指向有几个?

function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id)
      }
    }
  }
}
var f = foo.call({id: 1})

var t1 = f.call({id: 2})()()
var t2 = f().call({id: 3})()
var t3 = f()().call({id: 4}) 

答案是this的指向只有一个,就是函数foothis,最终输出三个id: 1,这是因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。所以不管怎么嵌套,t1t2t3都输出同样的结果。如果这个例子的所有内层函数都写成普通函数,那么每个函数的this都指向运行时所在的不同对象。

输出结果为:

id: 1
id: 1
id: 1

第六题

var name = 'window'
function Person (name) {
  this.name = name
  this.foo = function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo.call(person2)()
person1.foo().call(person2)

这是一道new绑定结合显式绑定的题目,具体分析如下

  • person1.foo.call(person2)()foo()函数内的this指向了person2,所以打印出person2,而内部返回的匿名函数是由window调用的,所以打印出window
  • person1.foo().call(person2)是将匿名函数的this显式绑定到了person2上,所以打印出来的会是person2

输出结果如下:

'person2'
'window'
'person1'
'person2'

第七题

var obj = {
  a: 1,
  foo: function (b) {
    b = b || this.a
    return function (c) {
      console.log(this.a + b + c)
    }
  }
}
var a = 2
var obj2 = { a: 3 }

obj.foo(a).call(obj2, 1)
obj.foo.call(obj2)(1)
  • 开始调用obj.foo(a)2传入foo函数并赋值给型参b,并且由于闭包的原因,使得匿名函数内能访问到b,之后调用匿名函数的时候,用call()改变了this的指向,使得匿名函数内this.a3,并传入最后一个参数1,所以第一行输出的应该是3 + 2 + 1,也就是6
  • 而第二行,obj.foo.call(obj2)这里是将foo函数内的this指向了obj2,同时并没有传递任何参数,所以b开始是undefined的,但是又因为有一句b = b || this.a,使得b变为了3;同时最后一段代码(1),是在调用匿名函数,且和这个匿名函数内的this应该是指向window的,因此输出为2 + 3 + 1,为6

输出结果为:

6
6

第八题

const obj = {
  aaa() {
    setTimeout(function() {   // 1.普通函数的setTimeout

      setTimeout(function() {
        console.log(this)     
      })
      setTimeout(() => {
        console.log(this)    
      })

    })

    setTimeout(() => {      // 2.箭头函数的setTimeout

      setTimeout(function(){
        console.log(this)   
      })
      setTimeout(() => {
        console.log(this)   
      })

    })
  }
}
obj.aaa()

第一个普通函数的setTimeout:this指向window

  • 普通函数的子setTimeout:默认绑定,this指向全局对象window
  • 箭头函数的子setTimeout:指向上层作用域的this,上层作用域的this就是window

第二个箭头函数的setTimeout:往上层作用域aaa找this,因为aaa是被obj调用的,aaa方法的this就是指向obj,所以this指向obj

  • 普通函数的子setTimeout:默认绑定,this指向全局对象window
  • 箭头函数的子setTimeout:指向上层作用域的this,上层作用域的this就是obj

所以最终依次输出:

window
window
window
obj

第九题

var name = 'window'
var obj1 = {
  name: 'obj1',
  foo1: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  },
  foo2: () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj2 = {
  name: 'obj2'
}
obj1.foo1.call(obj2)()
obj1.foo1().call(obj2)
obj1.foo2.call(obj2)()
obj1.foo2().call(obj2)

箭头函数的this无法通过bind、call、apply直接修改,但是可以通过改变作用域中this的指向来间接修改。接下来具体分析一下:

  • obj1.foo1.call(obj2)()第一层为普通函数,并且通过.call改变了this指向为obj2,所以会打印出obj2,第二层为箭头函数,它的this和外层作用域中的this相同,因此也是obj2
  • obj1.foo().call(obj2)第一层打印出obj1,第二层为箭头函数,使用了.call想要修改this的指向,但是并不能成功,因此.call(obj2)对箭头函数无效,还是打印出obj1
  • obj1.foo2.call(obj2)()第一层为箭头函数,并且想要通过.call(obj2)改变this指向,但是无效,且它的外层作用域是window,所以会打印出window,第二层为普通函数,this是最后调用者window,所以也会打印出window
  • obj1.foo2().call(obj2)第一层为箭头函数,外层作用域是window,打印出window,第二层为普通函数,且使用了.call(obj2)来改变this指向,所以打印出了obj2

输出结果如下:

'obj2' 'obj2' 
'obj1' 'obj1' 
'window' 'window' 
'window' 'obj2'

第十题

var obj = {
    hi: function(){
        console.log(this)
        return ()=>{
            console.log(this)
        }
    },
    sayHi: function(){
        return function() {
            console.log(this)
            return ()=>{
                console.log(this)
            }
        }
    },
    say: ()=>{
        console.log(this)
    }
}
let hi = obj.hi()      // 1
hi()                   // 2
let sayHi = obj.sayHi()
let fun1 = sayHi()     // 3
fun1()                 // 4
obj.say()              // 5
  1. obj.hi()对应了this的隐式绑定规则,this绑定在obj上,所以输出obj
  2. hi()这一步执行的就是箭头函数,箭头函数会往上一层作用域找this,刚刚我们得出上一层的this是obj,所以输出obj
  3. sayHi()属于隐式绑定丢失的情况,这个时候this执行的是默认绑定,this指向的是全局对象window
  4. fun1() 这一步执行的是箭头函数,箭头函数会往上一层作用域找this,this指向的是window,因此输出结果是window
  5. obj.say()执行的是箭头函数,当前的代码块obj中是不存在this的,只能往上找,就找到了全局的this,指向的是window

最终从1-5依次输出为:

obj
obj
window
window
window

OK,以上就是本篇文章的全部内容啦!

如果你看到了这里,首先感谢你能够花时间阅读我的文章,如果文章有哪里不当的地方,欢迎批评指正~如果你觉得文章对你有一点帮助的话,也不要吝啬你的点赞哦!

参考

阮一峰老师的ES6教程
嗨,你真的懂this吗?
this、call、apply、bind
【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)
web前端面试-面试官系列

推荐阅读

图解script的defer / async与DOMContentLoaded / Load
详解JavaScript作用域和作用域链
彻底搞懂作用域、执行上下文、词法环境
两个项目实例+常用语法解析带你掌握Mock.js✨
靠做题📝来掌握Promise/async/await
用大白话🙌带你掌握闭包
图文并茂🌈聊聊原型与原型链