JavaScript this 使用梳理

348 阅读6分钟

JavaScript 中的this关键字在开发中的角色是非常灵活的,基本上可以指代一切调用者,也正因为如此,成为了 JS 相关的面试中的重要考查点。对它的原理和使用的梳理还是很有必要的。

一句话总结

虽然this使用灵活多变,但是我们依然可以找出一定的规律进行概括。 一句话总结,那就是 this指向的是函数运行时的所在的环境对象。也有其他说取决于调用函数时的执行上下文,或者取决于函数调用位置的,虽然没错但个人觉得并不及这一句精炼。有了抽象概括,接着再来实际的场景中看看这一结论是如何得以体现的。

全局环境下的函数调用

通常来说,全局环境一般指浏览器和node环境,而关于this的考察一般是在浏览器环境中, 对于如下代码:

var a = 1;
function fr() {
  console.info("this.a", this.a);
  console.info("this", this);
}
fr();

在浏览器的环境中的输出是:

1
Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}

可见,直接函数调用的时候,this指向环境对象(浏览器也就是window)。不过,严格模式下上面代码的thisundefined,this.a会直接报错。

如果是在Node环境中呢?

undefined
Object [global] {...

Node环境下,的this指向的是全局global, a变量并没有写到全局,所以this.aundefined

因实际中关于考察 this 的考察大多指浏览器环境,后面对Node环境只作简单描述,不过多展开。

函数作为对象的方法调用

函数经常被挂载到一个对象上面,通过对象属性的方式进行调用。

const obj = {
  name: "te",
  say: function () {
    console.info("Hi !");
    console.info("this=", this);
  },
};
obj.say();

// Hi !
// this= { name: 'te', say: [Function: say] }

可见,函数作为对象的一个属性来调用的时候,this是指向这个上级对象的。此时函数运行所在的环境对象也是这个上级obj对象。

我们对上面的obj对象不做修改,对调用方式做简单修改:

const s = obj.say;
s();

此时的输出是:

Hi !
VM872:5 this= Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}

可以看到函数say中的this指向已经发生了变化,不再指向obj对象,而是window这个全局对象。是不是看起来有一点不可思议?

这是因为obj.say函数被赋值给s之后,对s的调用其实是在全局环境中的,所以函数内部的this又成了全局的window

构造函数中的 this

看到这里估计会有声音说,构造函数中的this不是最简单的吗,指向当前返回的新对象,但真的是这样吗?

关于new操作符创建新的对象的过程可以简单描述为:

  1. 创建新的空对象
  2. 为该对象添加书属性和方法,并使构造函数的this指向新对象。
  3. 返回该新对象

在实际使用new创建对象的时候,一般不会在构造函数中手动指定返回值,因为new是会返回一个符合预期的对象的。不过如果手动给构造函数添加了return,那么还是要注意区分的:

  • 显式return一个非对象的值,得到的是创建出的对象实例。
function ori() {
  this.name = "A";
  return;
}
const obj = new ori();
console.info("", obj);
//  ori { name: 'A' }

并没有返回值(相当于undefined),得到的是对象实例。这里return数值类型和字符串的话,构造函数的结果都是对象实例。

  • return一个对象,得到的就是该对象,相应的this也指向这个对象。
function ori() {
  this.name = "A";
  const inner = {
    name: "B",
  };
  return inner;
}
const obj = new ori();
console.info("", obj);
// { name: 'B' }

可以看到,这时的返回的对象已经是inner对象。所以,构造函数中的返回对象的this也是要视具体的return来决定的。

bind/call/apply 方法

三个方法都是可以改变对应函数的this指向的,bind方法通过该函数调用并且返回一个新的函数,新的函数已经修改了this指向,只有调用这个新的函数,函数体才会被执行。而call/apply方法是会直接绑定this并进行调用的,调用的时候,函数内部的this指向call/apply的第一个参数,两者的区分主要在于传参的格式上,其他深入点可以自行补充。

三者可以这样转换:

const obj = {};
fun.call(obj, "arg1","arg2",...);

相当于:

const obj = {};
fun.call(obj, ["arg1","arg2",...]);

bind则需要手动调用一次:

const obj = {};
fun.bind(target, "arg1","arg2",...)();

关于修改原函数中的this绑定,可以参考如下:

const personA = {
  name: "A",
  say: function () {
    console.info("my name is " + this.name);
  },
};
const personB = { name: "B" };
personA.say.call(personB);

// my name is B

最初是personA在执行自身的say方法,怎么就输出B了呢,因为call方法将personA.say函数中的this绑定到了personB,故输出的时候,this.name也就是personBname

箭头函数的 this

箭头函数作为ES6的特殊化函数,this的绑定比较特殊,不适用于上面根据运行时环境决定,只能通过定义的作用域链,也就是定义的外层的环境来决定。更加浅显一点来说,箭头函数的this指向父作用域,否则就是全局对象,call/apply/bind方法都是不能改变箭头函数的this指向的。

const name = "xx";
const personA = {
  name: "A",
  say: () => {
    console.info("my name is " + this);
  },
};
personA.say();

// my name is [object Window]

可以看到,父级对象personA的环境就是全局,say中的this也就是全局window

const name = "xx";
const personA = {
  name: "A",
  say: function () {
    let name = "In";
    return () => {
      console.info("my name is " + this.name);
    };
  },
};
personA.say()();

// my name is A

箭头函数的父级是say对应的匿名函数,所在的环境是在对象personA中,故personA.say()得到的箭头函数中的this就是指向personA的,所以最终的this.name就是A

现在可以看看call方法对箭头函数this的影响:

const personA = {
  name: "A",
  say: function () {
    return () => {
      console.info("my name is " + this.name);
    };
  },
};
const personB = { name: "B" };
personA.say.call(personB)();
personA.say().call(personB);

// my name is B
// my name is A

先对personA.say调用callsay对应的匿名函数的this指向被改变为personBsay对应的匿名函数的this取决于父级匿名函数this,调用时this也指向personB,故得到的this.nameB

而执行personA.say()之后,已经得到了箭头函数,此时再去调用call方法的话,是无法改变箭头函数中的this指向的,故this.name依然还是A