JavaScript中的this指向和易错前端面试题

3,078 阅读5分钟

初学者关于this的理解一直很模糊,关于this的面试题更加令人头大。

this是函数执行的主体(谁执行的) 请记住:this是谁和函数在哪儿创建的或者在哪执行的都没有

掌握以下几条分清执行的主体(this)的规律,大多数面试题可迎刃而解。

1. 给元素的某个事件绑定方法,当事件触发方法执行的时候,方法中的this是当前操作的元素(隐式绑定)

document.body.onclick = function() {
  // this: body
}

通常来说this的值是触发事件的元素的引用,这种特性在多个相似的元素使用同一个通用事件监听器时非常让人满意。

当使用 addEventListener() 为一个元素注册事件的时候,句柄里的 this 值是该元素的引用。其与传递给句柄的 event 参数的 currentTarget 属性的值一样。

2. 方法执行,看方法前面是否有点(.),如果有点,点前面是谁this就是谁(隐式绑定),没有点this就是window(在严格模式下("use strict")没有点thisundefined,也称为默认绑定)

注意:自执行函数(`(function(){...})()`)的this一般是window

function fn() {
  console.log(this)
}
let obj = {
  fn: fn
}
fn()
obj.fn()

输出:window对象 obj对象

上述代码等同于:

let obj = {
  fn: function fn() {
    console.log(this)
  }
}
fn()
obj.fn()

即在上文中,函数虽然被定义在对象的内部中,但它和”在对象外部声明函数,然后在对象内部通过属性名称的方式取得函数的引用”,这两种方式在性质上是等价的(而不仅仅是效果上)

在一串对象属性链中,this绑定的是最内层的对象

如下所示:

var obj = {
  a: 1,
  obj2: {
    a: 2,
    obj3: {
      a: 3,
      getA: function () {
        console.log(this.a)
      }
    }
  }
}
obj.obj2.obj3.getA()

输出:3,题中this:obj.obj2.obj3

3. 在构造函数模式执行中,函数体中的this是当前类的实例

执行new操作的时候,将创建一个新的对象,并且将构造函数的this指向所创建的新对象。JS编译器会做这四件事情:

  1. 创建一个新的空的对象
  2. 把这个对象链接到原型对象上
  3. 这个对象被绑定为this
  4. 如果这个函数不返回任何东西,那么就会默认return this
  function Fn(name) {
    // this: Fn类的实例
    this.name = name
  }
  var f1 = new Fn('qxj')
  var f2 = new Fn('lx')
  console.log(f1.name)
  console.log(f2.name)

输出:qxj lx

4. call/bind/apply可以改变this的指向(显式绑定)

call的基本使用方式: fn.call(object)
  fn:你调用的函数,object:你希望函数的this所绑定的对象。 fn.call(object)的作用

  1. 即刻调用这个函数(fn)
  2. 调用这个函数的时候函数的this指向object对象

例子:

var obj = {
  a: 1,          // a是定义在对象obj中的属性
  fire: function () {
    console.log(this.a)
  }
}
var a = 2        // a是定义在全局环境中的变量  
var fireInGrobal = obj.fire
fireInGrobal()   // => 2
fireInGrobal.call(obj) // => 1

原本丢失了与obj绑定的this参数的fireInGroba再次重新把this绑回到了obj

但是,我们其实不太喜欢这种每次调用都要依赖call的方式,我们更希望:能够一次性返回一个this被永久绑定到obj的fireInGrobal函数,这样我们就不必每次调用fireInGrobal都要在尾巴上加上call那么麻烦了。

怎么办呢?如果使用bind的话会更加简单

var fireInGrobal = function () {
    fn.call(obj)   //硬绑定
}

可以简化为:

var fireInGrobal = fn.bind(obj);

call和bind的区别:在绑定this到对象参数的同时:

  1. call将立即执行该函数

  2. bind不执行函数,只返回一个可供执行的函数

【其他】:apply除了使用方法,它和call并没有太大差别

5. 箭头函数

箭头函数会无视以上所有的规则,this的值就是函数创建时候所在的词法作用域(lexical scope)中的this,而和调用方式无关。可以对比下面两个例子:

function Person() {
  this.age = 0
  setTimeout(function () {
    console.log(this.age)     // => undefined
  }, 1000)
}
var p = new Person()

function Person() {
  this.age = 10
  setTimeout(() => {
    console.log(this.age)     // => 10
  }, 1000)
}
var p = new Person()

在上面没有使用箭头函数的例子当中,setTimeout内部的函数是被global调用的,而global没有age这个属性,因此输出undefined

第二个例子使用了箭头函数,this就会使用lexical scope中的this,就是Person,因此输出10。

根据MDN文档,箭头函数的this值为它被创建时的环境,虽然箭头函数内的this指向不可通过bind、call、apply等操作改变,但是外部的环境的this是可以的,换句话说要想改变箭头函数的this,只需改变外部环境的this即可,参考以下的例子:

const obj = {
  name: 'this指向obj',
  bar() {
    const x = () => this
    return x
  },
}
console.log(obj.bar()())
const a = {name: 'this指向a'}
const b = {name: 'this指向b而非obj和a'}
console.log(obj.bar.bind(b)().call(a))

输出:

改变箭头函数的this指向

【总结】绑定优先级

  1. 箭头函数
  2. 关键字new调
  3. 显式绑定
  4. 隐式绑定
  5. 默认绑定

箭头函数优先级最高,会无视2-5绑定规则。而默认绑定优先级最低,只有其他绑定都不使用的时候,才会使用默认绑定。

6. 几个面试题

eg1:

var fullName = 'language'
var obj = {
  fullName: 'javascript',
  prop: {
    getFullName: function () {
      return this.fullName
    }
  }
}
console.log(obj.prop.getFullName())
var test = obj.prop.getFullName
console.log(test())

输出:javascript language

此题符合第二条规律,方法执行时obj.prop.getFullName()getFullName()前方有点,thisobj.prop,输出obj.prop.fullNamejavascripttest()前面没有点,this为window,输出window.fullNamelanguage

eg2:

var val = 1
var json = {
  val: 10,
  dbl: function () {
    val *= 2
  }
}
json.dbl()
console.log(json.val + val)

输出:12

此题符合第二条规律,方法执行时json.dbl()dbl()前方有点,this为json,但是dbl方法中的val前面没有this.,因此其实用的是全局的valjson.val == 10,全局的val==2,那么json.val + val => 12

题目变换一下:

var val = 1
var json = {
  val: 10,
  dbl: function () {
    this.val *= 2
  }
}
json.dbl()
console.log(json.val + val)

输出:21

var val = 1
var json = {
  val: 10,
  dbl: function () {
    this.val *= 2				// json.val = 20
  }
}
json.dbl()			// 方法前面有点,this: json
console.log(json.val + val)		// 20 + 1 => 21

eg3:

var num = 10
var obj = {num: 20}
obj.fn = (function (num) {
  this.num = num * 3
  num++
  return function (n) {
    this.num += n
    num++
    console.log(num)
  }
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)

输出:22 23 65 30 图解:

eg3