从this指向的面试题看this绑定规则

258 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第二天,点击查看活动详情

最近刷到了几道this相关的面试题,感觉还挺有意思的,拿出来和大家分享一下,顺带也复习一下this绑定相关的知识点。

this绑定的优先级

this绑定的优先级一般是这样排序的:

new绑定 > 硬绑定(bind) > 显式绑定(apply/call) > 隐式绑定 > 默认绑定(兜底规则)

并且函数的this与函数声明的位置无关,只与函数的调用方式有关。箭头函数本身没有this,如果在箭头函数中使用this,那么会继承外层函数调用的this绑定,如果没有的话,会找到全局作用域下的this,也就是window。

知道绑定的优先级后,一起来看看这几道面试题吧:

面试题一:

var name = "window";
var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};
//写出每次打印的内容
function sayName() {
  var fn = person.sayName;
  fn(); 
  person.sayName(); 
  (person.sayName)(); 
  (fn1 = person.sayName)();
}
sayName();

这里调用sayName函数后,执行函数体代码,首先是将person对象中的sayName方法赋值给了fn函数,然后对fn函数进行了独立函数调用,独立函数调用是应用默认绑定规则的,所以这里首先会打印window。

第二行打印通过.点操作符对person对象中的sayName方法进行调用,那么会应用隐式绑定的规则,所以会打印person。

然后通过小括号()对person.sayName进行了包裹,再进行调用。但是.操作符优先级本身就比()小括号高,所以这里还是会打印person。

最后通过小括号()包裹了一个fn1 = person.sayName的表达式,这个表达式的返回值就是person中的sayName方法对应的函数。这一行就相当于对这个函数也进行了独立函数调用,所以也会打印window。

所以面试题一的答案就是window/person/person/window。你答对了吗

面试题二:

var name = 'window'
var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () { 
    return () => {
      console.log(this.name)
    }
  }
}
var person2 = { name: 'person2' }

//写出每次打印的内容
person1.foo1(); 
person1.foo1.call(person2); 

person1.foo2(); 
person1.foo2.call(person2);

person1.foo3()(); 
person1.foo3.call(person2)(); 
person1.foo3().call(person2); 

person1.foo4()();  
person1.foo4.call(person2)();
person1.foo4().call(person2);

这里第一段代码直接通过点.操作符调用了person1的foo1方法,会应用隐式绑定的规则,打印person1。然后通过call方法对person1的foo1方法进行调用,会应用显式绑定的规则,打印person2。

第二段代码通过点.操作符调用了person1的foo2方法,但是由于foo2是箭头函数,自身没有this,所以沿着作用域会向上查找,找到全局的this,打印window。同样的call调用对箭头函数也是无效的,所以后面也会打印window

第三段代码第一行先通过person1.foo3(),拿到foo3的返回值也是一个函数,然后对这个函数进行了独立函数调用,应用默认绑定的规则,打印window。第二行是对person1的foo3函数进行了call调用,foo3的this指向person2,但是跟foo3的返回值中的函数没什么关系,所以还是会打印window。第三行先通过person1.foo3()拿到返回值函数,然后对返回值函数进行call调用,打印person2。

第四段代码第一行通过person1.foo4(),拿到foo4的返回值是一个箭头函数,然后调用该箭头函数,因为箭头函数自身没有this,所以沿着函数调用的作用域向上查找,找到foo4的作用域,foo4是通过person1.进行调用的,所以应用隐式绑定,打印person1。第二行通过call调用了foo4,所以箭头函数找到foo4的this时,会打印person2。第三行对箭头函数自身使用call无效,找到foo4时,还是打印person1。

所以面试题二的答案是:

person1/person2
window/window/
window/window/person2
person1/person2/person1

面试题三:

var name = 'window'
function Person(name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

//写出每次打印的内容
person1.foo1() 
person1.foo1.call(person2) 

person1.foo2() 
person1.foo2.call(person2) 

person1.foo3()()  
person1.foo3.call(person2)()  
person1.foo3().call(person2)  

person1.foo4()() 
person1.foo4.call(person2)()  
person1.foo4().call(person2)

第一段代码第一行,通过点.操作符调用了person1的foo1方法,隐式绑定,打印person1。第二行,通过call调用foo1,显式绑定,打印person2。

第二段代码第一行,通过点.操作符调用person1的foo2方法,但是foo2是箭头函数,自身没有this,沿作用域向上查找,在构造函数Person中找到this为person1,打印person1。第二行通过call调用箭头函数无效,还是打印person1。

第三段代码第一行,通过person1.foo3()拿到返回值是一个函数,然后进行调用,这里也相当于独立函数调用,默认绑定,打印window。第二行,通过call调用person1的foo3,foo3的this指向person2,但是这和返回值函数也没什么关系,还是相当于独立函数调用,打印window。第三行,通过call调用返回值函数,显式绑定,打印person2。

第四段代码第一行通过person1.foo4(),拿到foo4的返回值是一个箭头函数,然后调用该箭头函数,因为箭头函数自身没有this,所以沿着函数调用的作用域向上查找,找到foo4的作用域,foo4是通过person1.进行调用的,所以应用隐式绑定,打印person1。第二行通过call调用了foo4,所以箭头函数找到foo4的this时,会打印person2。第三行对箭头函数自身使用call无效,找到foo4时,还是打印person1。

所以面试题三的答案是:

person1/person2
person1/person1
window/window/person2
person1/person2/person1

面试题四:

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

//写出每次打印的内容
person1.obj.foo1()()
person1.obj.foo1.call(person2)() 
person1.obj.foo1().call(person2) 

person1.obj.foo2()() 
person1.obj.foo2.call(person2)() 
person1.obj.foo2().call(person2)

第一段代码第一行,通过person1.obj.foo1()拿到foo1的返回值函数,进行独立函数调用,这里也是打印window。第二行,通过call调用foo1拿到返回值函数,这里foo1的this是person2,但是和返回值函数没什么关系。对返回值函数来说还是独立函数调用,打印window。第三行通过call调用返回值函数,显式绑定,打印person2。

第二段代码第一行,通过person.obj.foo2()拿到foo2的返回值是一个箭头函数,调用该箭头函数。箭头函数自身没有this,沿作用域向上查找,找到foo2的作用域,foo2是通过obj点.操作符调用的,所以这里会找到obj,打印obj。第二行通过call把foo2的this显式绑定为person2,箭头函数调用时,找到foo2的this,打印person2。第三行通过call调用箭头函数无效,箭头函数向上找到foo2的this还是obj,所以打印obj。

所以面试题四的答案是:
window/window/person2
obj/person2/obj

总结

上面四道面试题循序渐进的考察了this的几种绑定规则,你都做对了吗? 其实只要搞清楚this的几种绑定规则以及它们的优先级,在实际开发中遇到this都能合理的运用。

再回顾下优先级: new绑定 > 硬绑定(bind) > 显式绑定(apply/call) > 隐式绑定(对象调用)> 默认绑定(兜底规则)

并且函数的this与函数声明的位置无关,只与函数的调用方式有关。箭头函数本身没有this,如果在箭头函数中使用this,那么会继承外层函数调用的this绑定,如果没有的话,会找到全局作用域下的this,也就是window。