JavaScript中的this

262 阅读5分钟

this 是什么

在 JavaScript 中并没有 OOP 编程的概念,我们谈到的 this 不同于一些 OOP 编程里指向的实例化对象,它指的是运行时的上下文。所谓上下文,就是运行时所在的环境对象,比如你在公司,可能你的领导是你的部门经理,在家就是你媳妇儿一样,不同的场合上下文是不一样的。

this 的应用场景

在 JavaScript 中函数具有定义时上下文、运行时上下文以及上下文可改变的特点,也就是说函数中的 this 在不同的场合对应不同的值。在变量对象与作用域链一文中我们谈到 this 的确定是在执行环境的创建阶段完成的,也就是说 this 在运行时是基于执行环境绑定的,在全局执行函数,this 就指向全局(浏览器中为 window),如果函数作为一个对象的方法调用时,this 就指向这个对象。

全局调用

来看下面的的例子。

例 1:

如下,函数 getName 在全局调用,this 指向全局对象

var name = 'lily';
function getName() {
  var name = 'lucy';
  console.log('name:', this.name);
};
//非严格模式下等同于window.getName()
getName();
=> name: lily

例 2:

如下,尽管函数 getNameFunc 为 boy 对象的方法,但因其在全局调用,this 同样指向全局对象。

var name = 'lily';
var boy = {
  name: 'lucy',
  getName: function() {
    console.log('name:', this.name);
  },
};
var getNameFunc = boy.getName;
getNameFunc();
=> name: lily

例 3:

如下,boy.getName()返回一个匿名函数,假如这个匿名函数叫做 f,则(boy.getName())()等同于f(),等同于在全局中调用,因此 this 同样指向全局对象。

var name = 'lily';
var boy = {
  name: 'lucy',
  getName: function() {
    var name = 'snow';
    return function() {
      console.log('name:', this.name);
    }
  },
};

(boy.getName())();

=> name: lily

为了保持 this 可以通过闭包实现,如下,执行 boy.getName()时,this 指向当前执行环境 boy,因此 that 指向 boy,属性 name 为 lily,匿名函数执行console.log('name:', that.name);时,由于作用域链的关系,可以访问到上级作用域的 that 对象,指向 boy,因此 that.name 为 lucy

var name = 'lily';
var boy = {
  name: 'lucy',
  getName: function() {
    var name = 'snow';
    var that = this;
    return function() {
      console.log('name:', that.name);
    }
  }
};

(boy.getName())();

=> name: lucy

对象调用

如下,对象 boy 调用自己的方法 getName,this 则指向 boy。

var boy = {
  name: 'lucy',
  getName: function() {
    console.log('name:', this.name);
  }
};

boy.getName();
=> name: lucy

构造函数调用

想要知道调用构造函数 this 如何指向,需要知道 new 操作符究竟做了什么。 如下为 new 的模拟实现:

function mockNew(f) {
  // 1.
  var returnObj, proto;
  var newObj = new Object();
  // 2.
  proto = Object(f.prototype) === f.prototype ? f.prototype : Object.prototype;

  // 3.
  newObj.__proto__ = proto;
  // 4.
  /*
    arguments为类数组对象需要通过Array.prototype.slice.call将其转换为数组;
    Array.prototype.slice.call(arguments, 1)中的1是为了去掉arguments的第一个参数(函数f),而没有从0开始;
    通过f.apply调用,则将this指向了newObj。
  */
  returnObj = f.apply(newObj, Array.prototype.slice.call(arguments, 1));

  // 5.
  // 检查returnObj是否为Object类型
  if (Object(returnObj) === returnObj) {
    return returnObj;
  }
  return newObj;
}

通过 new 操作符调用构造函数(利用内置[[Construct]]方法),会经历以下几个阶段:

    1. 初始化内部变量
    1. 给内置属性[[prototype]](__proto__)赋值

      如果 f 的prototype为原始的 Object 类型,则将构造函数 f 的 prototype 赋值给 proto,否则将 Object 的 prototype 赋值给 proto。

    1. 创建继承自 proto 的对象

      通过newObj.__proto__ = proto,使newObj 可通过原型链继承 proto 的属性。

    1. 绑定 this,将其值设置为第三步生成的对象

      4.1. 通过 f.apply 调用 f,等同于 newObj.f(), 在构造函数 f 中执行 this.xxx = xxx;等同于执行 newObj.xxx = xxx,相当于 this 绑定了 newObj。

      4.2. 调用构造函数,可能返回一个对象 returnObj。

    1. 返回新生成的对象

      如果第 4 步中的 returnObj 值为 Object 类型,则 new 操作最终返回这个对象 returnObj,否则返回第 4 步中绑定this的的 newObj。

来看下面的例子:

通过 mockNew 函数构造对象的过程中,会调用上述第 4 步 f.apply(newObj, Array.prototype.slice.call(arguments, 1)),等同于调用 newObj.f(...arguments),则 this.name = 'lily'等同于 newObj.name = 'lily',mockNew 返回 newObj 时,p 就等于 newObj,因此 p 能够访问到 person 的 name 属性。

function person() {
  this.name = 'lily';
}

//这里,可以认为mockNew(person)等同于new person()
var p = mockNew(person);
p.name // lily

来看另一个例子:

var human = {
  name: 'lucy',

}

//返回了一个对象,则new操作符最终返回这个对象
function person() {
  this.name = 'lily';
  return human;
}

var p = mockNew(person);
p.name // lucy

由此,通过 new 操作符调用构造函数时,this 的最终指向为 new 返回的对象,即新创建的对象 newObj 或构造函数中返回的对象 returnObj(上例中的 human)。

func.call 和 func.apply

func.call 和 func.apply 的作用一样都是改变执行上下文,只是接收参数的形式不同。 func.apply 方法传入两个参数,第一个参数是想要指定的上下文,为空则指向全局对象,第二个参数是函数参数组成的数组。 func.call 方法传入两个参数,第一个参数是想要指定的上下文,第二个参数是传入的是一个参数列表,而不是单个数组。

/*
  thisArg: 想要指定的环境
  argsArray: 参数数组
*/
func.apply(thisArg, argsArray)

/*
  thisArg: 想要指定的环境
  arg1、arg2...: 参数列表
*/
func.call(thisArg, arg1, arg2, ...)

如下 boy 并没有 getName 方法,但是通过 apply/call 改变 this 的指向达到了在 boy 中调用 girl 的 getName 方法。

  function getName(firstName, lastName) {
    console.log(`${firstName}.${this.name}.${lastName}`)
  };
  const girl = {
    name: 'lucy',
    getName,
  };
  const boy = {
    name: 'Jeffrey'
  };
  //相当于boy.getName(['Michael', 'Jordan'])
  girl.getName.apply(boy, ['Michael', 'Jordan']);
  girl.getName.call(boy, 'Michael', 'Jordan');
  => Michael.Jeffrey.Jordan

bind 函数

bind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数。

const newGetName = girl.getName.bind(boy);
newGetName('Michael', 'Jordan')
=> Michael.Jeffrey.Jordan

综上,this 的指向由其具体的执行环境决定,同时也可以通过函数的原型方法 apply、call 以及 bind 来显式地改变 this 的指向。不过,箭头函数的this,总是指向定义时所在的对象,而不是运行时所在的对象,apply、call也无法更改。