全方位解读this

126 阅读10分钟

一、前言

说到this,我提出了下面几个问题:

  • this存放在哪里?
  • this是如何出现,又是如何消失的?
  • this有什么作用?

从定义来看

this是在执行上下文创建时确定的一个在执行过程中不可更改的变量。

this的指向和上下文密不可分的,下面我们通过执行上下文来全面理解this

二、执行上下文

当 JS 引擎解析到可执行代码片段(通常是函数调用阶段)的时候,就会先做一些执行前的准备工作,这个 “准备工作”,就叫做 "执行上下文(execution context 简称 EC)" 或者也可以叫做执行环境。

1、执行上下文类型

javascript 中有三种执行上下文类型,分别是:

  • 全局执行上下文 —— 这是默认或者说是最基础的执行上下文,一个程序中只会存在一个全局上下文,它在整个 javascript 脚本的生命周期内都会存在于执行堆栈的最底部不会被栈弹出销毁。

  • 函数执行上下文 —— 每当一个函数被调用时,都会创建一个新的函数执行上下文(不管这个函数是不是被重复调用的)

  • Eval 函数执行上下文 —— 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于并不经常使用 eval,所以在这里不做分析。

2、执行上下文生命周期

我们先看看执行上下文的生命周期

执行上下文的创建阶段,会分别生成变量对象,建立作用域链,确定this指向。通过执行上下文的生命周期我们可以得知,this的指向是在执行上下文创建是被确定的

3、实例分析

下面通过一段代码来具体分析一下执行上下文:

add() // call b
console.log(a) // undefined

var a = 'Hello world'

function add() {
	console.log('call b')
}

代码执行前,JavaScript 引擎会创建全局执行上下文,在这一阶(创建阶段),会进行变量和函数的初始化声明,变量统一定义为 undefined(var情况下) 需要等到赋值时才会有确值,而函数则会直接定义,确定 this 值为全局对象(以浏览器为例,就是 window ),你可以参考下图:

从图中可以看出,代码中全局变量和函数都保存在全局上下文的变量环境中。

在上下文创建阶段,引擎检查代码找出变量和函数声明,变量最初会设置为 undefined(var 情况下),或者未初始化(let 和 const 情况下)。这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。

执行上下文准备好之后,便开始执行全局代码(执行阶段),当执行到 add 这儿时,JavaScript 判断这是一个函数调用,那么将执行以下操作:

  • 首先,从全局执行上下文中,取出 add 函数代码。
  • 其次,对 add 函数的这段代码进行编译(函数被调用),此时会创建函数的执行上下文(通过执行上下文生命周期可知道函数中的this指向此时被确定)。
  • 最后,执行代码,输出结果。

就这样,当执行到 add 函数的时候,我们就有了两个执行上下文了——全局执行上下文和 add 函数的执行上下文。

4、执行上下文总结

首先,我们需要得出一个非常重要的,并且一定要牢记于心的结论

  • 函数中this的指向,是在函数被调用的时候确定的。也就是执行上下文被创建时确定的。

因此,这个特性也导致了this的多变性:🙂即当函数在不同的调用方式下都可能会导致this的值不同。

var a = 10;
var obj = {
  a: 20
}

function fn() {
  console.log(this.a);
}

fn(); // 10
fn.call(obj); // 20

三、重看 this

相信根据上文内容大家应该已经明白什么是JavaScript执行上下文

我们再来看this,其实它也存放在执行上下文中

执行上下文包括了:变量环境、词法环境、this。如下图所示:

从图中可以看出,this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this

执行上下文主要分为三种:

1、 全局执行上下文

2、 函数执行上下文

3、 eval 执行上下文

所以对应的 this 也只有这三种:

1、 全局执行上下文中的 this

2、函数中的 this

3、 eval 中的 this(先不讲解此情况)

四、全局执行上下文中的 this

console.log(this) // window

console.log(this === window) // true

var a = 1;
function fun() {
   var a = 2;
   return this.a;
}
fun(); //1

我们在全局对象中调用 fun,实际上就相当于 window.fun() 的一个调用,那么就是指向 Window。

注意这里是非严格模式。严格模式下的全局对象是 undefined,函数fun中this则指向undefined,
此时就会报错误 Uncaught TypeError: Cannot read property 'name' of undefined
另外注意:开启了严格模式,只是说使得函数内的this指向undefined,它并不会改变全局中this的指向。

五、函数执行上下文中的 this

说到函数中this绑定这里就说说this绑定规则

this的绑定规则有4种:

  • 默认绑定(严格/非严格模式)
  • 隐式绑定
  • 显式绑定
  • new绑定

1、默认绑定

当函数独立调用的时候,在严格模式下它的this指向undefined,在非严格模式下,当this指向undefined的时候,自动指向全局对象(浏览器中就是window)

function foo() {
    console.log( this.a ); // 2
}
var a = 2;

// 调用
foo();


// --------------------------------------
function foo() { // 运行在严格模式下,this会绑定到undefined
    "use strict";
    console.log( this.a );
}

var a = 2;

// 调用
foo(); // TypeError: Cannot read property 'a' of undefined

// --------------------------------------

function foo() { // 运行
    console.log( this.a );
}

var a = 2;

(function() { // 严格模式下调用函数则不影响默认绑定
    "use strict";
    foo(); // 2
})();

2、隐式绑定

什么是隐式绑定呢,如果函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上。

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

var obj = {
    a: 2,
    greet: function () {
      console.log(this.a);  
    },
    foo: foo
};

obj.foo(); // 2
隐式丢失

被隐式绑定的函数特定情况下会丢失绑定对象,应用默认绑定,把this绑定到全局对象

  var a = 1;
  var obj = {
    a: 2,
    b: function() {
      return this.a;
    }
  }
  var t = obj.b;
  console.log(t());//1

这时候它又变成 window 指向了,此刻 var t = obj.b 只是一个定义,真正执行是在 t()。

3、显示绑定

显示绑定很好理解,我们希望this绑定在哪个对象上我们就用方法绑定它,具体有三种方法可以达到这个效果,需要注意的是一旦我们 显示绑定 之后我们便无法再绑定了。

  • call()
  • apply()
  • bind()
let bar = {
  myName : "dell",
}
function foo(){
  this.myName = "dellyoung"
}
foo.call(bar)
console.log(bar) // {myName:"dellyoung"}
console.log(myName) // 报错myName未定义

4、new 绑定

function foo(a) {
    this.a = a;
}

var bar = new foo(2); // bar和foo(..)调用中的this进行绑定
console.log( bar.a ); // 2

咱们现在再来看一下通过new调用构造函数到底做了什么:

function New(source, ...arg) {
    // 创建一个空的简单JavaScript对象(即{})
    let newObj = {};
    // 链接该对象(即设置该对象的构造函数)到另一个对象
    Object.setPrototypeOf(newObj, source.prototype); 
    // 将步骤1新创建的对象作为this的上下文 ;
    const resp = source.apply(newObj, arg);
    // 判断该函数返回值是否是对象
    if (resp && Object.prototype.toString.call(resp) === "[object Object]") {
        // 如果该函数没有返回对象,则返回this。
        return resp
    }
    // 如果该函数返回对象,那用返回的这个对象作为返回值。
    return newObj
}

五、疑难填坑

1、this存放在哪里

this存放在每个执行上下文中

2、this是如何出现,又是如何消失的?

this随着执行上下文出现,当执行上下文被回收后,也随之消失

3、this有什么作用?

全局执行上下文中:this指向了window对象,方便我们来调用全局window对象。

函数执行上下文中:this指向了调用该函数的对象,减少的参数的传递,原来如果需要在函数内部操作被调用对象,
当然还需要将对象作为参数传递进去,而有了this,就不需要了,直接拿this就能操作被调用对象的属性。

4、总结this绑定

  • 在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
  • 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
  • 谁调用函数,函数的this指向谁。对象调用函数自然不必说,全局环境调用其实也可以理解为window来调用,所以当然指向了window

六、箭头函数中this指向

箭头函数不像普通函数有多个规则,它不考虑绑定的四个规则,箭头函数中没有 this,它的this是根据它外层作用域来决定的,看下面这个例子

var obj = {
    count: 0,
    cool: function () {
        console.log(this); // obj 对象 
        setTimeout(() => {
            console.log(this); // obj对象
            this.count++;
            console.log("awesome?");
        }, 100);
    }
}
obj.cool();

七、小试牛刀

1、默认绑定

先介绍一种最简单的绑定方式吧:默认绑定。

也就是我们常说的:在非严格模式下this指向的是全局对象window,而在严格模式下会绑定到undefined

  • 第一题
var foo = 1;
function bar () {
    console.log(foo);
    console.log(window.foo);
    var foo = 10;
    console.log(foo);
}
bar();

解析:

我们知道在使用var创建变量的时候(不在函数里),会把创建的变量绑定到window上,所以此时foowindow下的属性。

而函数bar也是window下的属性。另外在调用bar时,创建函数执行上下文,在这个阶段引擎检查代码找出变量和函数声明,变量最初会设置为 undefined(var 情况下)

答案:

undefined
1
10
  • 第二题
var a = 1
function foo () {
  var a = 2
  function inner () { 
    console.log(this.a)
  }
  inner()
}
foo()

解析:inner中,this指向的还是window。

答案:

1
  • 第三题
let a = 10;
const b = 20;

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

console.log(window.a);

解析:如果把var改成了let 或者 const,变量是不会被绑定到window上的,所以此时会打印出三个undefined。

答案:

undefined
undefined
undefined

2、隐式绑定丢失问题

  • 第一题
function foo () {
  console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;

obj.foo();
foo2();

解析:

obj.foo()中this的指向是为obj的(可以看第二部分隐式绑定),所以obj.foo()执行的时候,打印出来的是obj对象中的a,也就是1。

foo2指向的是obj.foo函数,不过调用它的却是window对象,所以它里面this的指向是为window。

答案:

1
2
  • 第二题
function foo() {
  console.log(this.a);
}
function doFoo(fn) {
  console.log(this);
  fn();
}
var obj = { a: 1, foo };
var a = 2;
var obj2 = { a: 3, doFoo };

obj2.doFoo(obj.foo);

解析:

现在调用obj2.doFoo()函数,里面的this指向的应该是obj2,因为是obj2调用的它。

但是obj.foo()打印出来的a依然是2,也就是window下的。

答案:

{ a:3, doFoo: f }
2

3、显示绑定问题

function foo1 (b) {
  console.log(`${this.a} + ${b}`)
  return this.a + b
}
var a = 1
var obj = {
  a: 2
}

var foo2 = function () {
  return foo1.call(obj, ...arguments)
}

var num = foo2(3)
console.log(num)

答案:

'2 + 3'
5

4、综合题

var num = 1;
var myObject = {
    num: 2,
    add: function() {
        this.num = 3;
        (function() {
            console.log(this.num);
            this.num = 4;
        })();
        console.log(this.num);
    },
    sub: function() {
        console.log(this.num);
    },
    greet: function () {
         console.log(this);
        setTimeout(() => {
            console.log(this);
            this.count++;
            console.log("awesome?");
        }, 100);
}
myObject.add();
console.log(myObject.num);
console.log(num);
var sub = myObject.sub;
sub();
myObject.greet();

参考

1、【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理) this为何物

2、jsliang 求职系列 - 05 - this

3、说说执行上下文吧