首先我们要知道所有的 JavaScript 函数执行时都是有上下文的。

那么,什么是函数的执行上下文呢?简单说来就是函数中 this 所指向的对象。

  • 函数上下文就是函数中的 this
  • 函数中的 this 指向和函数的定义位置、执行位置无关
  • 函数中 this 的指向只取决于函数的调用方式

举个栗子:

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

当我们创建了一个函数的时候,我们并不知道此时的 this 指向什么,所以说创建函数的时候,我们并不知道 this 的指向,当调用函数的时候才决定了 this 的指向
不同的调用机制决定了函数上下文对象的不同:

1. 作为普通函数进行调用时,其上下文是全局对象window;
2. 作为(对象)方法进行调用时,其上下文对象就是该对象本身;
3. 作为构造器(通过 new xxx())进行调用时,其上下文是一个新分配的空对象;
4. 通过函数的apply()或者call()方法进行调用时,上下文可以设置成任意值;

1. 普通函数调用:
function foo() {
    console.log(this.name);
}
foo(); // undefined

当我们直接调用 foo 的时候,foo 是普通函数,其上下文全局对象是 window, window 上并没有设置 name 这个属性,所以 this.name 是 undefined。

2. 作为(对象)方法调用:
let obj = {
    name: 'lokka',
    foo: function() {
        console.log(this.name);
    }
}

obj.foo(); // lokka

当我们调用 obj 对象里的 foo 方法的时候,其上下文对象是 obj 本身,this 指向 obj ,obj 上有 name 属性,所以 this.name 就是 obj.name,所以是 lokka。

3. 作为构造器(通过 new xxx())调用:
function foo(val) {
    this.name = val;
}

let p1 = new foo("lokka");

let p2 = new foo("yaya");

console.log(p1.name); // lokka
console.log(p2.name);	// yaya

通过关键词 new 调用,通过 new 调用的函数就是我们通常说的构造函数,其上下文对象就是 new 出来的实例对象,实例对象继承了构造函数的属性和方法(即构造函数的 this 指向新创建的实例对象),所以 p1.name = val ,val 就是传入的 lokka 值,所以 p1.name 值为 lokka。

4. 通过函数的apply()或者call()方法调用:

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

let a = {
    name: "lokka"
}

let b = {
    name: "yaya"
}

foo.call(a); // lokka

foo.call(b); // yaya

当我们通过函数的apply()或者call()方法进行调用的时候,我们指向谁,其上下文对象就是谁,比如说我们通过 call 方法调用foo,同时指向 a 对象,那么 foo 函数上下文就是对象 a,所以 this.name 就是 a.name,即值为 lokka。

调用栈

创建执行上下文就是完成作用域链、参数对象,内部变量函数的初始化,以及this变量的获取。执行上下文包括什么呢?

  1. 作用域链。包括函数本身及所有父执行上下文,这也解释了为什么函数内部可以访问外部的变量,但外部不能内部的变量
  2. 参数 argument对象,内部变量、函数的声明。初始化参数对象arguments,解析函数内部,对变量的声明进行初始化(但不执行),函数的声明只是创建一个函数对象。
  3. this 变量

所有执行上下文构成了一个调用栈,所以全局调用栈永远在栈底。调用函数时,将执行上下文入栈,函数执行完成后则出栈。

代码的执行

这个时候,上下文已经入栈,执行上下文已经初始化了作用域链,内部变量,this等,然后就开始一步步的执行。查找变量的时候,先找当时函数的内部变量,找不到再一层层的去作用域链上找,一直找到全局作用域,如果仍旧没找到,则报错,变量未定义。