前端面试-JavaScript 中的 this

57 阅读5分钟

this 是 JavaScript 的一个关键字,一般指向调用它的对象,这句话其实有两层意思,首先 this 指向的应该是一个对象,更具体地说是函数执行的“上下文对象”。其次这个对象指向的是“调用它”的对象,如果调用它的不是对象或对象不存在,则会指向全局对象(严格模式下为 undefined)。

四种调用模式

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断

第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象

第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象

第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象

第四种是 apply 、 call 和 bind 调用模式

显式绑定 this

首先最容易想到的时显式绑定,包括 call,apply,bind

call 和 apply 用法功能基本类似,都是通过传入 this 指向的对象以及参数来调用函数。区别在于传参方式,前者为逐个参数传递,后者将参数放入一个数组,以数组的形式传递。bind 有些特殊,它不但可以绑定 this 指向也可以绑定函数参数并返回一个新的函数,当 c 调用新的函数时,绑定之后的 this 或参数将无法再被改变

function getName() {
  console.log(this.name);
}
var b = getName.bind({ name: "bind" });
b(); //"bind"
getName.call({ name: "call" }); //"call"
getName.apply({ name: "apply" }); //"apply"

由于 this 指向的不确定性,所以很容易在调用时发生意想不到的情况。在编写代码时,应尽量避免使用 this,比如可以写成纯函数的形式,也可以通过参数来传递上下文对象。实在要使用 this 的话,可以考虑使用 bind 等方式将其绑定

隐式绑定 this

全局上下文

function fn() {
  console.log(this);
}
fn(); // 浏览器:Window;Node.js:global

全局上下文默认 this 指向 window, 严格模式下指向 undefined

直接调用函数

let obj = {
  a: function() {
    console.log(this);
  },
};
let func = obj.a;
func();

这种情况是直接调用。this 相当于全局上下文的情况

对象.方法的形式调用

let obj = {
  a: function() {
    console.log(this);
  },
};
let func = obj.a();
func();

这种情况 this 指向这个对象

DOM 事件绑定

onclick 和 addEventerListener 中 this 默认指向绑定事件的元素。

IE 比较奇异,使用 attachEvent,里面的 this 默认指向 window。

new + 构造函数

此时构造函数中的 this 指向实例对象。

class A {
  fn() {
    console.log(this);
  }
}
var a = new A();
a.fn(); // a

箭头函数

箭头函数没有 this, 因此也不能绑定。里面的 this 会指向当前最近的非箭头函数的 this,找不到就是 window(严格模式是 undefined)

或者说箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this。可以简单地理解为箭头函数的 this 继承自上层的 this,但在全局环境下定义仍会指向全局对象

let obj = {
  a: function() {
    const fun = () => {
      console.log(this);
    };
    fun();
  },
};

obj.a(); // 找到最近的非箭头函数a,a现在绑定着obj, 因此箭头函数中的this是obj

箭头函数和普通函数相比,有以下几个区别

  • 不绑定 arguments 对象,也就是说在箭头函数内访问 arguments 对象会报错;
  • 不能用作构造器,也就是说不能通过关键字 new 来创建实例;
  • 默认不会创建 prototype 原型属性;
  • 不能用作 Generator() 函数,不能使用 yeild 关键字。

正常情况:this 指向运行时所在的对象而不是定义时所在对象

箭头函数:this 指向定义时所在的对象而不是使用时所在对象

优先级: new > call、apply、bind > 对象.方法 > 直接调用

稍复杂点的例子

  1. 如果在函数 fn2() 中调用函数 fn(),那么当调用函数 fn2() 的时候,函数 fn() 的 this 指向哪里呢?
function fn() {
  console.log(this);
}
function fn2() {
  fn();
}
fn2(); // ?

由于没有找到调用 fn 的对象,所以 this 会指向全局对象,答案就是 window(Node.js 下是 global)

  1. 让函数 fn2() 作为对象 obj 的属性,通过 obj 属性来调用 fn2,此时函数 fn() 的 this 指向哪里呢?
function fn() {
  console.log(this);
}
function fn2() {
  fn();
}
var obj = { fn2 };
obj.fn2(); // ?

调用函数 fn() 的是函数 fn2() 而不是 obj。虽然 fn2() 作为 obj 的属性调用,但 fn2()中的 this 指向并不会传递给函数 fn(), 所以答案也是 window(Node.js 下是 global)

  1. 对象 dx 拥有数组属性 arr,在属性 arr 的 forEach 回调函数中输出 this,指向的是什么呢?
var dx = {
  arr: [1],
};
dx.arr.forEach(function() {
  console.log(this);
}); // ?

关于 forEach,它有两个参数,第一个是回调函数,第二个是 this 指向的对象,这里只传入了回调函数,第二个参数没有传入,默认为 undefined,所以正确答案应该是输出全局对象。

类似的,需要传入 this 指向的函数还有:every()、find()、findIndex()、map()、some(),在使用的时候需要特别注意。

  1. 创建一个 fun 变量来引用实例 b 的 fn() 函数,当调用 fun() 的时候 this 会指向什么呢?
class B {
  fn() {
    console.log(this);
  }
}
var b = new B();
var fun = b.fn;
fun(); // ?

fun 是在全局下调用的,所以 this 应该指向的是全局对象。这个思路没有没问题,但是 ES6 下的 class 内部默认采用的是严格模式,实际上面代码的类定义部分可以理解为下面的形式。

class B {
  "use strict";
  fn() {
    console.log(this);
  }
}

而严格模式下不会指定全局对象为默认调用对象,所以答案是 undefined。

  1. 前面提到 this 指向的要么是调用它的对象,要么是 undefined,那么如果将 this 指向一个基础类型的数据会发生什么呢
[0].forEach(function() {
  console.log(this);
}, 0); //Number {0}

基础类型也可以转换成对应的引用对象。所以这里 this 指向的是一个值为 0 的 Number 类型对象