# 函数上下文——this

如果你是一个JS的初学者,this这一定是一个能够让你欲哭无泪的语法。像C#Java这类语言,如果类的方法是非静态的,那么this指向的就是类的实例,如果是静态方法,那么就指向类本身,而JSthis是可变的,在没有搞懂this之前,不妨忘记之前学过的知识,以利于学习。

它有一个更加贴切的名字,函数上下文,这个名字恰到好处的表达了this的含义。

当函数在不同的环境下执行,this的指向是不同的,简言之就是哪个对象调用的这个函数,this就指向那个对象。

# this的默认绑定

默认情况的this调用都是隐式绑定,即谁调用,指向谁。因此,这节所有内容都不考虑方法被强制指定this的情况

# 方法中的this

let person = {
  firstName: "Bill",
  lastName: "Gates",
  id: 123,
  fullName: function(){
    return this.firstName + " " + this.lastName;
  }
};

person.fullName() // Bill Gates

let fullName = person.fullName;

fullName(); // "undefined undefined"

如果在 ES6 的class中的话,this的指向分两种情况:

class App {
  static run() {
    console.log(this);
  }

  log() {
    console.log(this);
  }
}

对于静态方法中的this,默认指向的是当前class(如run方法的this指向App),对于非静态方法的this,默认指向的是当前class的原型对象(如log方法的this指向App.prototype)

# 单独的this

console.log(this); // globalThis

在单独使用时,拥有者是全局对象,this指的是全局对象,即globalThis,对于nodejsglobalThis指向的是global对象,而对于浏览器, globalThis指向的是window对象,这就是上小节内容为什么fullName()输出的内容是"undefined undefined",而对于WebWorker中,this指向self对象。

# 函数的this

function Person(){
  return this;
}

非严格模式中,函数的拥有者默认绑定this,因此,在函数中,this指的是全局对象globalThis

严格模式不允许默认绑定,因此,在函数中使用时,在严格模式下,this是未定义的undefined

# 事件处理中的this

<button onclick="this.style.display='none'">点击来删除我!</button>

this指向的是当前触发事件的元素,如上面例子中,this指向的是button

# 改变this的默认绑定方式

由于this的可变性,导致我们的程序可能会变的脆弱,因此,需要在某些时候明确this的指向。

# new

function Person(){
  console.log(this instanceof Person)
  console.log(new.target);
}

const people = new Person(); // Person Person

这是一个隐式改变this的调用,同时又是this绑定中优先级最高的调用。

有了这个手段,我们可以用来判断函数是否是被用于new的形式调用,下面的代码就是babel转码class的结果的节选。

ES6中为new引入了一个新的数据target,也可以用来判断是否是通过new调用的。

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

接下来就是一个比较迷惑也不迷惑的面试题了,如下:

const obj = {
  name: 'JohnYang',
  age: 28,
}
function Person(){
  console.log(this instanceof Person)
  console.log(new.target);
  console.log(this.name, this.age);
}
// 想强制指定this为obj
const MyPerson = Person.bind(obj);

const p = new MyPerson();

因为使用new调用时的this绑定优先级是最高的,所以其实MyPerson就是Personthis上不存在nameage,因此最终输出的是undefined undefined

# callapplybind

这几个问题是面试题中考场最多的问题之一了,面试官:“请问callapplybind三者有什么区别,并说出如何实现”?

本文不讨论它们的实现,只讨论用法。

interface Function {
  apply(this: Function, thisArg: any, argArray?: any): any;
  call(this: Function, thisArg: any, ...argArray: any[]): any;
  bind(this: Function, thisArg: any, ...argArray: any[]): any;
}

首先,callapply几乎是差不多的,它们的第一个参数都是this上下文,而call是接受N个参数,一字排开,而apply是接受一个由函数的N个参数组成的数组(类数组对象也可以的),然后这个函数就被立即执行了。

如:

let firstName = "John";
let lastName = "Yang";
let person = {
  firstName: "Bill",
  lastName: "Gates",
  id: 123,
  fullName: function (arg1, arg2) {
    console.log(arg1, arg2);
    return this.firstName + " " + this.lastName;
  },
};
person.fullName.apply(person, ["hello", "world"]); // "hello" "world" 返回 "Bill Gates"
const fullName2 = person.fullName.bind(window, "hello", "world"); // "hello" "world" 返回 "John Yang"

bind和它们两者有个最大的不同,bind是返回一个被改变了this指向的函数

bind在使用的过程中还可以预制一些参数,那么得到的函数在执行的时候就可以省略掉预制的参数,如:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const MyPerson = Person.bind(window, "Tom");

const p = new MyPerson("28"); // { name:'Tom', age: '28' }
const yang = new MyPerson("Yang", "28"); // { name:'Yang', age: '28' }

# 箭头函数

箭头函数的this其实本质并不是改变了this的默认绑定,但是为了说明改变this指向的手段,就将其暂且的列在这一小节了。

class MyButton extends React.Component {
  onClick = () => {
    console.log(this.name);
  };

  render() {
    return <button onClick={this.onClick}>Hello World</button>;
  }
}

ES6中引入的新语法,箭头函数的this永远指向当前的父作用域的this,如在React的组件中可以经常看到上述代码。

接下来看一段转码前与转码后的代码。

转码前:

class A {
  name = "A";

  say = () => {
    console.log(this.name);
  };
}

const D = () => {
  console.log(this);
};

转码后:

var _this2 = void 0;

var A = /*#__PURE__*/ _createClass(function A() {
  var _this = this;

  _classCallCheck(this, A);

  _defineProperty(this, "name", "A");

  _defineProperty(this, "say", function () {
    console.log(_this.name);
  });
});

var D = function D() {
  console.log(_this2);
};

# this绑定的优先级

说完了this在实际开发中的场景之后,最后来聊一下this绑定的优先级的问题。

对于隐式绑定(或默认绑定)来说优先级是最低的(谁调用,指向谁),其次是使用callapplybind进行的显示绑定,优先级次之,最后是通过new调用的new绑定,优先级最高。

这儿并没有讨论箭头函数的this的优先级,因为箭头函数的this绑定在父级作用域下,是babel通过定义特定作用域下的变量,转化使得函数的最终的this能够符合预期,是语法糖,属于作用域链的范畴,跟this绑定无关。