JavaScript 中令人迷惑的this

145 阅读4分钟

前言

作为一个前端程序员,对于this这个关键字,前几年我经常选择避而不谈,平时开发遇到this指向不清楚时,我总是选择有礼貌的console.log(this);但是每次离职换工作时,总是会在我眼前晃悠;下定决心弄懂它,于是查阅了很多大佬的分享的博客;通过这篇文章总结分享一下。

this到底指向什么

说这个问题之前我先分享几段平平无奇的js代码

console.log(this); // window

// 定义一个函数
function foo() {
  console.log(this);
}
// 直接调用
foo() //window

// 作为对象数组调用
var xiaoming = {
  name"shengming",
  foo: foo
}
xiaoming.foo() //xiaoming{name: 'shengming', foo: ƒ}

foo.apply('test') // String {'test'}

从上面可以看出在不同情况下this的指向不同的,全局作用域中直接打印this,this是指向我们的全局(浏览器中是window)的,而在我们的函数作用域中,this会根据函数不同的调用方式而发生改变,通过查阅资料我得到了一些的结论:

  • this是函数调用时,函数执行上下文中的一个属性
  • this是再函数运行时,动态绑定的
  • this绑定和它定义的位置无关,只和它的调用方式和调用的位置有关系 光看结论,肯定还是难以理解的,所以下面具体说说this的绑定规则。

this的绑定规则

默认绑定
当函数没有绑定到任何对象中,而是直接调用时,这时候的this绑定就是默认绑定,而默认绑定一般指向全局,也就是window,可以运行下面测试代码加以验证

function foo() {
  console.log(this); // window
}
foo();

function foo1() {
  console.log(this); // window
  foo2();
}
function foo2() {
  console.log(this); // window 
}

foo1();

隐式绑定
当函数是通过某对象发起调用时,这时候的this绑定就是隐式绑定,而this绑定就会绑定到发起调用对象上,如下面demo调用方法

function Person() {
  console.log(this); // xiaoming对象
}

var xiaoming = {
  name"shengming",
  fooPerson
}
xiaoming.foo();

显示绑定
所谓显示绑定就是我们主动通过apply、call、bind等函数方法明确的把this绑定到我们想要的目标对象上,从而改变this指向。

function foo() {
  console.log(this);
}
foo.call({}) // {}
foo.apply([]) // []
var xiaoming = foo.bind('test')
xiaoming() // String{}

new关键字绑定
当函数作为构造函数时,我们需要用new关键字调用函数创建对象

// 创建Person
function Person(name) {
  console.log(this); // Person {}
  this.name = name; // Person {name: "shengming"}
}
var xiaoming = new Person("shengming");
console.log(xiaoming);

new关键字调用会执行以下步骤(来源mdn)

  1. 创建一个空的简单JavaScript对象(即 {} );
  2. 为步骤1新创建的对象添加属性 __proto__ ,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this。 所以这里很显然的会为对象绑定this指向

js this绑定规则就有以上四种,当四种绑定规则同时出现多个时js会优先使用哪个一个规则呢?所以这里会涉及到绑定规则的优先级

this绑定规则的优先级

结论:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
直接记住结论就好了,不要问为什么,问就是不知道。其实很多东西就没有为什么,如果不相信的话可以一一的写demo验证一下。

规则以外的特殊情况

凡是都有例外,this绑定规则也有些特殊情况,下面就列举我了解到的一些特殊情况

当显示绑定遇上undefined和null

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

var xiaoming = {
  name"shengming"
}
foo.call(xiaoming); // xiaoming对象
foo.call(null); // window
foo.call(undefined); // window
var bar = foo.bind(null);
bar(); // window

函数的间接引用使用默认绑定

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

var xiaoming = {
  name"shengming",
  foo: foo
}; 

var xiaoting = {
  name"xiaoting"
}

obj1.foo(); // xiaoming对象
(xiaoting.foo = xiaoting.foo)();  // window

箭头函数
箭头函数不使用上述四种绑定规则,而是根据调用时的外层作用域来决定this的指向

var xiaoming = {
  son: [],
  getSON() => {
    setTimeout(() => {
      console.log(this); // window
    }, 63072000000);
  }
}

xiaoming.getSon();

小试牛刀(看一道恶心的面试题)

var name = 'window'
var xiaoming = {
  name'shengming',
  fun1function () {
    console.log(this.name)
  },
  fun2() => console.log(this.name),
  fun3function () {
    return function () {
      console.log(this.name)
    }
  },
  fun4function () {
    return () => {
      console.log(this.name)
    }
  }
}

var xiaoting = { name'xiaoting' }

xiaoming.fun1(); //shengming,隐式绑定
xiaoming.fun1.call(xiaoting); // xiaoting 显示绑定优先级高于隐式绑定

xiaoming.fun2();//window 箭头函数绑定规则失效,它的上级作用域为全局
xiaoming.fun2.call(xiaoting);//window 箭头函数绑定规则失效,它的上级作用域为全局

xiaoming.fun3()();//window 获取到函数全局调用所以默认绑定
xiaoming.fun3.call(xiaoting)();//window 获取到函数全局调用所以默认绑定
xiaoming.fun3().call(xiaoting);//xiaoting 显示绑定

xiaoming.fun4()();//shengming //箭头函数找上层作用域
xiaoming.fun4.call(xiaoting)();//xiaoting //箭头函数找上层作用域
xiaoming.fun4().call(xiaoting);//shengming //箭头函数找上层作用域