首先我们要知道所有的 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变量的获取。执行上下文包括什么呢?
- 作用域链。包括函数本身及所有父执行上下文,这也解释了为什么函数内部可以访问外部的变量,但外部不能内部的变量
- 参数 argument对象,内部变量、函数的声明。初始化参数对象arguments,解析函数内部,对变量的声明进行初始化(但不执行),函数的声明只是创建一个函数对象。
- this 变量
所有执行上下文构成了一个调用栈
,所以全局调用栈永远在栈底。调用函数时,将执行上下文入栈,函数执行完成后则出栈。
代码的执行
这个时候,上下文已经入栈,执行上下文已经初始化了作用域链,内部变量,this等,然后就开始一步步的执行。查找变量的时候,先找当时函数的内部变量,找不到再一层层的去作用域链上找,一直找到全局作用域,如果仍旧没找到,则报错,变量未定义。