执行上下文即代码的执行环境。在 JS 代码的执行过程中,引擎会为我们创建“执行上下文栈”(也叫调用栈)。
执行上下文的分类
执行上下文主要分为三类:
- 全局上下文 —— 全局代码所处的环境,不在函数中的代码都在全局执行上下文中
- 函数上下文 —— 在函数调用时创建的上下文
- Eval 执行上下文 —— 运行 Eval 函数中的代码时所创建的环境
执行上下文的生命周期
创建阶段
执行上下文的初始化状态,此时一行代码都还没有执行,只是做了一些准备工作,具体包括:
- 创建全局对象(Window 有了);
- 创建 this ,并让它指向全局对象;
- 给变量和函数安排内存空间;
- 默认给变量赋值为 undefined;将函数声明放入内存;
- 创建作用域链
执行阶段
逐行执行脚本里的代码,执行赋值操作
🤔 变量提升
console.log(name) // undefined
var name = 'xiuyan'
JS 引擎不会抛出变量未声明的错误,而是会输出一个 undefined 值,表现得好像这个 name 变量早已被声明过一样。像这样的现象,我们叫它 “变量提升”。
结合我们的上下文创建过程,所谓的 “提升”,只是变量的创建过程(在上下文创建阶段完成)和真实赋值过程(在上下文执行阶段完成)的不同步带来的一种错觉。执行上下文在不同阶段完成的不同工作,是 “变量提升 “的本质。
函数上下文和全局上下文的区别
- 创建的时机 —— 全局上下文在进入脚本之初就被创建,而函数上下文则是在函数调用时被创建
- 创建的频率 —— 全局上下文仅在代码刚开始被解释的时候创建一次;而函数上下文由脚本里函数调用的多少决定,理论上可以创建无数次
- 创建阶段的工作内容不完全相同 —— 函数上下文不会创建全局对象(Window),而是创建参数对象(arguments);创建出的 this 不再死死指向全局对象,而是取决于该函数是如何被调用的 —— 如果它被一个引用对象调用,那么 this 就指向这个对象;否则,this 的值会被设置为全局对象或者 undefined(在严格模式下)
调用栈
函数执行完毕后,其对应的执行上下文也随之消失了。这个消失的过程,我们叫它“出栈”。
因为函数上下文可以有许多个,我们不可能保留所有的上下文。当一个函数执行完毕,其对应的上下文必须让出之前所占用的资源。因此上下文的建立和销毁,就对应了一个” 入栈 “和” 出栈 “的操作。当我们调用一个函数的时候,就会把它的上下文推入调用栈里,执行完毕后出栈,随后再为新的函数进行入栈操作。
function testA() {
console.log('执行第一个测试函数的逻辑');
testB();
console.log('再次执行第一个测试函数的逻辑');
}
function testB() {
console.log('执行第二个测试函数的逻辑');
}
testA();
以上这个脚本的执行上下文栈,随着代码的执行,会经历一个这样的过程:
-
执行之初,全局上下文创建:
-
执行到 testA 调用处,testA 对应的函数上下文创建:
-
执行到 testB 处,testB 对应的函数上下文创建:
-
testB 执行完毕,对应上下文出栈,剩下 testA 和 全局上下文:
-
testA 执行完毕,对应执行上下文出栈,剩下全局上下文:
作用域
当代码在一个环境中执行时,会创建变量对象的一个作用域链.作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问.
作用域的前端始终都是当前执行的代码所在环境的变量对象.如果这个环境是函数,则将其活动对象作为变量对象.全局执行环境的变量对象始终都是作用域链中的最后一个对象。
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColors(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
//这里可以访问color,anotherColor,tempColor
}
//这里可以访问color,anotherColor,但不能访问tempColor
swapColors();
}
//这里只能访问color
changeColor();
作用域是” 访问变量的一套规则 “,作用域其实就是当前所处的执行上下文。我们基于执行上下文,来理解一下作用域的特征:
- 作用域对外隔离
全局作用域 相对于 testA 的函数作用域,它是外部作用域;全局作用域、testA 相对于 testB 的函数作用域,它们都是外部作用域。我们知道,作用域在嵌套的情况下,外部作用域是不能访问内部作用域的变量的。 - 闭包 —— 特殊的 “弹出”
一般来说,函数出栈后,我们都没有办法再访问到函数内部的变量了。但闭包可不是这样。在执行上下文的创建阶段,跟着被创建的还有作用域链!这个作用域链在函数中以内部属性的形式存在,在函数定义时,其对应的父变量对象就会被记录到这个内部属性里。闭包正是通过这一层作用域链的关系,实现了对父作用域执行上下文信息的保留。 - 自由变量的查找 —— 作用域链与上下文变量的结合
引擎会沿着作用域链向上查找变量。这里是沿着作用域链找,可不是沿着调用栈一层一层往上找哦!调用栈是在执行的过程中形成的,而作用域链可是在书写阶段就决定了。因此,testB 里找不到的变量,绝不会去 testA 里找,而是去全局上下文变量里找!
function show(){
var b = 1;
var a = ++b;
}
show();
print(a); //报错
function show(){
var b = 1;
a = ++b; //全局变量a
}
show();
print(a); //2