(一) js执行上下文
1. 执行上下文
执行上下文 (上下文), 其实就是js代码运行时所处的环境
简而言之: 上下文就是一个运行时环境
我们知道, js中的变量有全局变量和局部变量之分
- 全局变量: 定义在 全局环境
- 局部变量: 定义在 函数内部
只有有了全局环境, 才能有函数环境
对于全局环境, 我们又称之为 全局上下文, 对于函数内部环境, 称之为 函数上下文
-
全局上下文是打开浏览器就会自动创建的
-
函数上下文是在函数被调用的时候创建的
在上下文创建以后, 并不会立即执行我们写的js代码, 而是会先进行一个名为 **预编译 **的过程, 根据上下文的不同, 预编译又可以分为:
- 全局预编译
- 函数预编译
对于以上概念, 可以用一个例子说明:
假设城市是最高层 (忘掉省,国等概念), 我们要去一个陌生的城市 A , 在A城市中的 B 公司 进行工作 (执行代码), 那么城市A和公司B对我们而言, 是全新的生活环境 (上下文环境), 对于公司而言, 肯定是得有城市, 才能有公司, 因为我们假设城市是最高层, 因此 城市A就是 全局环境 (全局上下文), 公司B就是 局部环境(函数上下文)
既然城市A和公司B是全新的环境, 那我们对于新环境是不是得先 熟悉一段时间 (预编译), 当我们 (js代码) 先来到 城市A (全局环境), 要先对 城市A进行一个熟悉 (全局预编译), 熟悉得差不多之后, 我们就可以直接去要工作的B公司 (执行全局代码), 来到B公司以后, 又要对公司进行一个熟悉 (函数预编译), 熟悉差不多之后, 就可以在公司干活了 (函数代码执行)
工作了一段时间后, 我们发现, 这里不适合我们, 于是打道回府, 离开了公司B (函数上下文释放), 离开了城市A (全局上下文释放 => 浏览器关闭)
2. 预编译
预编译是上下文创建之后, js代码执行前的一段时期, 在这个时期, 会对js代码进行预处理
那么进行什么样的处理呢 ?
在解决这个问题之前, 先引入一个新的概念 变量对象VO (Variable Object)
2.1 变量对象VO
每个执行上下文都有一个与之相关联的变量对象 (Variable Object, 简称 VO), 当前执行上下文中所有的变量
和函数
都保存在其中
2.2 全局预编译
全局上下文创建之后, 会进行全局预编译, 全局预编译会进行如下预处理:
- 寻找变量声明, 变量名作为VO对象的属性名, 属性值置为
undefined
- 寻找函数声明, 函数名作为VO对象的属性名, 属性值为函数本身
- 如果函数名与变量名冲突, 函数声明会将变量声明覆盖
预编译结束以后, 再进行逐行代码执行
什么意思?
示例
console.log(a) // function a () { }
var a = 100;
function a () { }
console.log(a) // 100
代码执行之前:
首先会创建全局执行上下文, 并有与之对应的变量对象VO被创建
VO {}
然后寻找变量声明: var a; 变量名作为VO对象的属性名, 属性值置为 `undefined`
VO: {a:undefined}
寻找函数声明: function a () { } 函数名作为VO对象的属性名, 属性值为函数本身
但是此时函数名与变量名冲突, 函数声明会将变量声明覆盖,因此
VO: {a: function a () { }}
预编译至此结束, 开始代码执行
首先打印 a function a () { }
接着为a赋值为100
函数声明, 跳过
再次打印a, 上面a被赋值为100, 因此输出100
2.3 函数预编译
上面我们说过: 函数执行上下文是在函数被调用的时候创建的
只要函数不被调用, 那么就不会进行函数的预编译
而函数预编译与全局预编译, 唯一的不同之处就在于: 函数可能会有形参
还有一点区别就是: VO在函数预编译阶段被叫做AO (活动对象), 两者本质一样, 只不过换了个说法
函数预编译的过程:
- 寻找变量声明, 变量名作为AO对象的属性名, 属性值置为
undefined
- 寻找形参, 形参名作为AO对象的属性名, 属性值置为
undefined
- 如果形参名与变量名冲突, 形参会将变量声明覆盖
- 将实参的值赋予形参, 即替换 AO对象中形参的属性值
- 寻找函数声明, 函数名作为AO对象的属性名, 属性值为函数本身
- 如果函数名与变量名冲突, 函数声明会将变量声明覆盖
示例
function a(b, c) {
console.log(b); // 1
var b = 0
console.log(b); // 0
var b = function () {
console.log('bbbb')
}
console.log(c); // undefined
console.log(b); // function () {console.log('bbbb')}
}
a(1)
例题
function foo() {
console.log(a);
a = 1;
}
foo(); // ???
function bar() {
a = 1;
console.log(a);
}
bar(); // ???
3. 执行上下文栈
栈: LIFO 先进先出的一种数据结构
我们知道, 函数里面可以嵌套函数, 不同的函数调用又会形成不同的执行上下文, 这些不同的上下文环境我们同一放进执行上下文栈中进行管理, 栈底为全局上下文, 每当有一次函数调用, 形成的函数上下文就会被push进栈顶, 函数执行完, 该函数所对应的函数上下文将会被pop出上下文栈
示例
function fn1() {
function fn2() {
console.log(1);
}
fn2()
}
fn1()
上述代码共在上下文栈中push了几个上下文?
首先有一个全局的上下文, 接着进行全局预编译, 接着fn1被调用, 会创建fn1的上下文, 压入执行上下文栈, 然后进行fn1的预编译, 接着调用fn2, 创建fn2的上下文,压入执行上下文栈, 然后fn2预编译, 接着执行fn2的打印语句, fn2执行完毕后, 对应的执行上下文从执行上下文栈中pop出, 此时fn1也被执行完, 对应的执行上下文也从上下文栈中pop出
理解上下文对后面作用域链和闭包的理解起到了重要作用