执行上下文与执行上下文栈
首先介绍下执行上下文和执行上下文栈的原理,从而引出变量提升和闭包的底层原理。
这里先简单提下变量提升的原理和含义,具体底层实现请继续往下看。
变量提升与函数提升
-
提升的内部原理
函数在运行的时候,会首先创建执行上下文,然后将执行上下文入栈,然后当此执行上下文处于栈顶时,开始运行执行上下文。
在创建执行上下文的过程中会做三件事:创建变量对象,创建作用域链,确定 this 指向,
其中创建变量对象的过程中,首先会为 arguments 创建一个属性,值为 arguments,然后会扫码 function 函数声明,创建一个同名属性,值为函数的引用,接着会扫码 var 变量声明,创建一个同名属性,值为 undefined,这就是变量提升。
-
变量声明提升
- 通过var定义(声明)的变量, 在定义语句之前就可以访问到
- 值: undefined
- 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
-
函数声明提升
- 通过function声明的函数, 在之前就可以直接调用
- 值: 函数定义(对象)
- 如果是通过函数表达式定义一个函数,属于变量提升,不属于函数提升,不可直接调用
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
-
问题: 变量提升和函数提升是如何产生的?
- 先有变量提升, 再有函数提升(函数提升优先级高于变量提升,同名的话函数会覆盖变量)
执行上下文
-
代码分类(位置)
- 全局代码
- 函数代码
-
全局执行上下文
- 在执行全局代码前将window确定为全局执行上下文
- 对全局数据进行预处理
- var定义的全局变量==>undefined, 添加为window的属性
- function声明的全局函数==>赋值(fun), 添加为window的方法
- this==>赋值(window)
- 开始执行全局代码
-
函数执行上下文
- 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象
- 对局部数据进行预处理
- 形参变量==>赋值(实参)==>添加为执行上下文的属性
- arguments==>赋值(实参列表), 添加为执行上下文的属性
- var定义的局部变量==>undefined, 添加为执行上下文的属性
- function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
- this==>赋值(调用函数的对象)
- 开始执行函数体代码
生命周期
执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段
创建阶段
创建阶段即当函数被调用,但未执行任何其内部代码之前
创建阶段做了三件事:
- 确定 this 的值,也被称为
This Binding
(this的值是在执行的时候才能确认) - LexicalEnvironment(词法环境) 组件被创建
- VariableEnvironment(变量环境) 组件被创建
- 创建作用域链
词法环境
词法环境有两个组成部分:
- 全局环境:是一个没有外部环境的词法环境,其外部环境引用为
null
,有一个全局对象,this
的值指向这个全局对象 - 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了
arguments
对象,外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境
dart
GlobalExectionContext = { // 全局执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Object", // 全局环境
// 标识符绑定在这里
outer: <null> // 对外部环境的引用
}
}
FunctionExectionContext = { // 函数执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Declarative", // 函数环境
// 标识符绑定在这里 // 对外部环境的引用
outer: <Global or outer function environment reference>
}
}
变量环境
变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性
在 ES6 中,词法环境和变量环境的区别在于前者用于存储函数声明和变量( let
和 const
)绑定,而后者仅用于存储变量( var
)绑定
ini
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
执行上下文如下
yaml
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: { // 词法环境
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: { // 变量环境
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
let
和const
定义的变量a
和b
在创建阶段没有被赋值,为< uninitialized >
,但var
声明的变量从在创建阶段被赋值为undefined
这就是变量提升的实际原因
执行阶段
在这阶段,执行变量赋值、代码执行
如果 Javascript
引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配 undefined
值
回收阶段
执行上下文出栈等待虚拟机回收执行上下文
执行上下文栈
-
在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
-
在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
-
在函数执行上下文创建后, 将其添加到栈中(压栈)
-
在当前函数执行完后,将栈顶的对象移除(出栈)
-
当所有的代码执行完后, 栈中只剩下window
面试题(妙啊)
javascript
/*
测试题1: 先变量提升,再函数提升
*/
function a() {}
var a;
console.log(typeof a) //'function'
/*
测试题2:
*/
if (!(b in window)) {
var b = 1;
}
console.log(b) //undefined
/*
测试题3:
*/
var c = 1
function c(c) {
console.log(c)
var c = 3
}
c(2) //报错!!! 秒啊!!!
//以上代码相当于
var c
function c(c) {
console.log(c)
var c = 3
}
c=1
c(2)
//先变量提升,再函数提升,再赋值!!!
复习
markdown
- 理解
- 执行上下文: 由js引擎自动创建的对象, 包含对应作用域中的所有变量属性
- 执行上下文栈: 用来管理产生的多个执行上下文
- 分类:
- 全局: window
- 函数: 对程序员来说是透明的
- 生命周期
- 全局 : 准备执行全局代码前产生, 当页面刷新/关闭页面时死亡
- 函数 : 调用函数时产生, 函数执行完时死亡
- 包含哪些属性:
- 全局 :
- 用var定义的全局变量 ==>undefined
- 使用function声明的函数 ===>function
- this ===>window
- 函数
- 用var定义的局部变量 ==>undefined
- 使用function声明的函数 ===>function
- this ===> 调用函数的对象, 如果没有指定就是window
- 形参变量 ===>对应实参值
- arguments ===>实参列表的伪数组
- 执行上下文创建和初始化的过程
- 全局:
- 在全局代码执行前最先创建一个全局执行上下文(window)
- 收集一些全局变量, 并初始化
- 将这些变量设置为window的属性
- 函数:
- 在调用函数时, 在执行函数体之前先创建一个函数执行上下文
- 收集一些局部变量, 并初始化
- 将这些变量设置为执行上下文的属性
作用域与作用域链
作用域
-
理解
- 就是一块"地盘", 一个代码段所在的区域
- 它是静态的(相对于上下文对象), 在编写代码时就确定了
- 这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
静态作用域:输出1
动态作用域:输出2
scss
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); //1
- 分类
- 全局作用域
- 函数作用域
- 没有块作用域(ES6有了)
- 作用
- 隔离变量,不同作用域下同名变量不会有冲突
- 为什么要有块级作用域
- 解决变量覆盖问题
- 解决循环变量变成全局变量
作用域与执行上下文
- 区别1
- 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
- 函数执行上下文环境是在调用函数时, 函数体代码执行之前创建
- 区别2
- 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
- 执行上下文环境是动态的, 调用函数时创建, 函数调用结束时上下文环境就会被释放
- 联系
- 执行上下文环境(对象)是从属于所在的作用域
- 全局上下文环境==>全局作用域
- 函数上下文环境==>对应的函数使用域
作用域链
- 理解
- 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找的
- 查找一个变量的查找规则
- 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
- 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
- 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
完整执行过程
ini
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
执行过程如下:
总结:
- 函数被创建,保存作用域链到 内部属性[[scope]]
- 创建 checkscope 函数执行上下文,并压栈
- 函数执行前的准备工作:
- 复制函数[[scope]]属性创建作用域链,
- 初始化活动对象,加入形参、函数声明、变量声明
- 将活动对象压入 checkscope 作用域链顶端
- 执行函数
- 出栈
1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
lua
checkscope.[[scope]] = [
globalContext.VO
];
2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ini
ECStack = [
checkscopeContext,
globalContext
];
3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
lua
checkscopeContext = {
Scope: checkscope.[[scope]],
}
4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
css
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
5.第三步:将活动对象压入 checkscope 作用域链顶端
css
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
css
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ini
ECStack = [
globalContext
];
所以闭包的底层原理就可以知道了
闭包底层原理
从执行上下文的角度来看:
压栈入栈顺序为:全局入栈---fn1入栈---fn1出栈---fn2入栈---fn2出栈---全局出栈
问题:fn1的执行上下文已经被销毁了,为什么fn2还可以访问fn1的变量a??
理由:fn2的作用域链还保存着fn1的活动对象
ini
fn2Context = {
Scope: [AO, fn1Context.AO, globalContext.VO],
}
面试题(妙啊)
javascript
//面试题1
var x = 10;
function fn() {
console.log(x); //10 因为作用域在函数定义时就已经确定了,fn的上级是window
}
function show(f) {
var x = 20;
f();
}
show(fn);
//面试题2
var fn = function () {
console.log(fn) //function () {console.log(fn) }
}
fn()
var obj = {
fn2: function () {
console.log(this.fn2) //function () {console.log(fn2);console.log(fn2);}
console.log(fn2) //报错,找不到fn2
}
}
obj.fn2()
复习
- 理解:
- 作用域: 一块代码区域, 在编码时就确定了, 不会再变化
- 作用域链: 多个嵌套的作用域形成的由内向外的结构, 用于查找变量
- 分类:
- 全局
- 函数
- js没有块作用域(在ES6之前)
- 作用
- 作用域: 隔离变量, 可以在不同作用域定义同名的变量不冲突
- 作用域链: 查找变量
- 区别作用域与执行上下文
- 作用域: 静态的, 编码时就确定了(不是在运行时), 一旦确定就不会变化了
- 执行上下文: 动态的, 执行代码时动态创建, 当执行结束消失
- 联系: 执行上下文环境是在对应的作用域中的