结合执行栈、执行上下文,理解this的指向问题

1,737 阅读8分钟

关于js中的this指向,必须深刻理解下面这句话:

  • this指向是在执行时确定的,不是定义时确定的
  • this指向是在执行时确定的,不是定义时确定的
  • this指向是在执行时确定的,不是定义时确定的

在介绍this指向之前,首先来解答由上面这句话引申出来的一个问题:“定义”时确定了什么,“执行”时又确定了什么呢? 以下面的代码为例展开说明:

function f1(){
    var x = 10;
    f2();
}
function f2(){
    console.log(x);
}
f1();

定义时 -- 作用域

作用域决定代码块内“资源”的可见性。作用域在定义时就确定,并且不会改变。

在给出上面代码执行的结果之前,我们先来分析一下代码定义到执行的过程。首先,在定义时确定代码的作用域:

  • 全局作用域:在全局作用域中只有两个变量f1和f2,分别指向一个函数;
  • f1函数作用域:在f1函数作用域中,只有一个变量x;
  • f2函数作用域:在f2函数作用域中,没有定义变量
    作用域
    至此,代码的作用域确定。作用域的确定,代码块内“资源”的可见性随即确定:
  • 全局作用域内可访问的变量:f1和f2;
  • f1函数作用域内可访问的变量:x和全局作用域内的所有可见的变量;
  • f2函数作用域内可访问的变量:全局作用域内的所有可见的变量

也许这时候你更加确定代码的执行结果,但是这里暂不揭晓,我们继续分析。

执行时 -- 执行上下文

执行上下文是在代码执行时确定,当函数被调用时,会在执行栈中创建一个新的执行上下文,并入栈顶。 (对于执行栈和执行上下文的理解可以参考此篇文章,也可以参考我翻译以后的译文)

上述代码段中,f1在全局作用域内被调用。下面我们分析一下函数调用和执行的具体过程:

  • f1函数在全局执行上下文中被调用;
  • 在f1函数执行前,创建新的执行上下文并入栈顶;
  • 执行f1函数。调用函数f2,再次创建新的执行上下文并入栈顶;
  • 执行f2函数。由于执行上下文中作用域链是由f2-->window,首先在f2函数作用域中没有找到x,向上查找至window全局作用域,window中也没有定义x,因此执行结果是:Uncaught ReferenceError: x is not defined

相信通过以上的分析,你对作用域和执行上下文已经有所了解。总结以上两个阶段我们可以简单的描述为:

  • 定义阶段:
    • 确定作用域:即确定当前代码块内“资源”的可见性。
  • 执行阶段:
    • 确定执行上下文:作用域链、VO/AO、this的值

但是,这些和this的指向有什么关系呢?其实回顾刚刚的定义过程和执行过程,你会发现,在定义阶段根本就没有涉及到this,只有在执行阶段才有this的出现。因此,你也就明白了本文最开始的一句话:this指向是在执行时确定的,而不是在定义时确定的。

this指向

关于this指向,相信大家看到过很多描述,比如:

  • 如果函数作为对象的方法时,方法中的 this 指向该对象;
  • 构造函数中的this,指向new出来的新对象
  • 普通函数在全局中调用,this指向window
  • ....

面对这么多情况,我们该怎么记忆?其实,this指向没有这么多规则,归根结底只有一句话:this总是指向当前函数所在的执行上下文 (注:箭头函数后面会单独介绍)。 结合这句话,我们仅从定义和执行两个角度再去分析常见的场景:

场景一:对象中的函数

var x = 1;
var obj = {
    x: 10,
    foo: function (){
        var x = 100;
        console.log(this.x); 
    }
}
obj.foo(); // 打印10
- 定义阶段:
  - 全局作用域可访问的变量:x 和 obj ,其中obj的属性:x 和 foo
- 执行阶段:
  - 调用obj.foo(),创建新的执行上下文
      - 确定作用域链和变量对象,其中foo的作用域链为:foo-->obj-->window
      - 确定this的值,foo所在的执行上下文为obj,所以foo中的this指向obj
  - 执行obj.foo(),控制台中打印10

场景二:全局作用域中的函数

function fun1 (){
    console.log(this);
}
fun1(); // 打印window
- 定义阶段:
  - 全局作用域可访问的变量:fun1 
- 执行阶段:
  - 调用fun1(),相当于调用window.foo(),创建新的执行上下文
      - 确定作用域链和变量对象,其中fun1的作用域链为:fun1 --> window
      - 确定this的值,fun1所在的执行上下文为window,所以fun1中的this指向window
  - 执行func1(),控制台打印window

场景三:构造函数

function Student (name,age){
    this.name = name;
    this.age = age;
}
Student.prototype.show = function (){
    console.log(this.name);
}
var andy = new Student('andy',10);
andy.show(); // 打印andy
- 定义阶段:
  - 全局作用域可访问的变量:Student、andy 
- 执行阶段:
  - 调用new Student('andy',10),其中new完成的操作如下所示:
      - 创建空对象,即var obj = {}
      - 当前空对象的原型指向构造函数的prototype对象,即obj.__proto__ = Object.create(Student.prototype)
      - this指向当前对象obj
      - 给当前空对象赋值,即obj.name = 'andy',obj.age = 10
      - 返回当前空对象obj
  - 执行andy.show(),通过原型链的方式调用Student中的show()方法,show方法当前所在的执行上下文为andy,因此控制台打印andy

场景四:回调函数

var x = 1;
var obj1 = {
    x:10,
    foo: function (){
        setTimeout(function (){
            console.log(this.x);
        },1000)
    }
}
obj1.foo(); // 打印1
- 定义阶段:
  - 全局作用域可访问的变量:x、obj1 
- 执行阶段:
  - 调用obj1.foo(),创建新的执行上下文
      - 确定作用域链和变量对象,其中foo的作用域链为:foo --> obj --> window
      - setTimeout函数是window的内置函数,因此setTimeout中回调函数的执行上下文为window,则回调函数中的this指向window
  - 执行obj1.foo(),控制台打印1

场景五:call,bind,apply

var x = 1;
var obj2 = {
    x:10,
    foo: function (){
        console.log(this.x);
    }
}
var obj3 = {
    x: 100
}
obj2.foo.call(obj3); // 打印100
call函数是改变“调用者”的执行上下文(即this指向)并立即执行“调用者”
- 定义阶段:
  - 全局作用域可访问的变量:x、obj2、obj3,其中obj2的属性包括x和foo,obj3的属性包括x
- 执行阶段:
  - 调用obj2.foo.call(obj3), ,将“调用者”foo的执行上下文改为call函数的第一个参数obj3
  - foo的执行上下文更改为新的执行上下文obj3
      - 确定作用域链和变量对象,其中foo的作用域链由为:foo --> obj2 --> window 更改为 foo --> obj3 --> window
      - 确定this的值,foo所在的执行上下文为obj3,所以foo中的this指向obj3
  - 执行obj2.foo.call(obj3),控制台打印100
 
 apply和call的作用相同,只是第二个参数的数据类型是数组。bind也是改变调用者的执行上下文,不同于call和apply的地方是,函数不会立即执行。

总结以上情况,我们发现每一个this值的确定过程都涉及到了执行上下文,因此this总是指向当前函数所在的执行上下文。但是,箭头函数中的this指向也是这么确定的吗?

箭头函数中的this

本文一开始我们就提到一句话,this指向是在执行时确定的,不是在定义时确定的。这句话在箭头函数中还适用吗?我们来看下面的例子:

var x = 1;
var obj = {
    x: 10,
    foo: () => {
        console.log(this.x);
    }
}
obj.foo();
我们还是按照定义和执行两个角度进行分析:
- 定义阶段:
  - 全局作用域可访问的变量:x、obj
- 执行阶段:
  - 调用obj.foo(),创建新的执行上下文
      - 确定作用域链和变量对象,其中foo的作用域链为:foo --> obj --> window
      - 确定this的值,foo所在的执行上下文为obj,所以foo中的this指向obj
  - 执行obj.foo(),控制台打印10

实际结果是我们分析的10吗?执行代码发现不是10,而是1。这是为什么呢?因为箭头函数中没有this,箭头函数体内的this === 最靠近箭头函数的 绑定this的 普通函数的 this值。再看下面的例子:

var x = 1;
var obj = {
    x: 10,
    foo: function (){
        var x = 100;
        setTimeout(() => {
            console.log(this.x);
        },1000)
    }
}
obj.foo();
- 定义阶段:
  - 全局作用域可访问的变量:x、obj
- 执行阶段:
  - 调用obj.foo(),创建新的执行上下文
      - 确定作用域链和变量对象,其中foo的作用域链为:foo --> obj --> window
      - 确定this的值,foo所在的执行上下文为obj,所以foo中的this指向obj
      - foo内部调用setTimeout函数,其中回调函数为箭头函数
  - 执行obj.foo()
      - 执行回调函数,其中this位于回调函数内部,所以沿着作用域链向上查找,发现foo中有this,并且this绑定在obj中,因此回调函数中的this也指向obj
      - 打印结果为10

总结

  • this指向是在执行时确定的,不是在定义时确定的;
  • 箭头函数中的this指向 ===> 最靠近它的 绑定this指向的 函数中的 this的值
以上是我个人的近期学习总结,如有不妥之处,还望各位指出