作用域 & 执行上下文
先看一段源代码,分析一下变量提升和预编译的过程:
var GLOBAL = {}
function fn(c) {
console.log(c) // true
var c = false
console.log(a) // function a(){}
var a = 1
console.log(a) // 1
function a() {
// wo can do something
}
console.log(a) // 1
b() // 函数声明
var b = function () {
console.log('函数表达式')
}
function b() {
console.log('函数声明')
}
b() // 函数表达式
var d = 4
}
fn(true)
JS 解释引擎工作流程:
- 当页面打开时,JS 解释引擎会首先查看 JavaScript 代码中有没有低级的语法错误,如果有,代码就不会执行。如果没有,js 代码就会进入预解析(编译)阶段。
- 预解析阶段,首先创建 GO 全局对象,然后逐行的执行代码,碰到函数执行,则创建 AO 局部对象。
JS 的执行分为:解释和执行两个阶段
- 解释阶段
- 词法分析
- 语法分析
- 作用域规则确定
- 执行阶段
- 创建执行上下文
- 执行函数代码
- 垃圾回收
一、什么是 GO(Global Object):
- 含义:GO 是指全局作用域的全局对象(window)
- 形成时间:页面打开时
- 销毁时间:页面关闭时
_js 全局预编译阶段,GO 对象的形成步骤:
- 创建一个 GO{}对象
- 找变量申明,将变量名作为 GO 对象的属性名,值是 undefined
- 找函数申明,值赋值函数体(function)
比如上面
二、什么是 AO(Activation Object)
- 含义:AO 是指局部作用域的局部对象
- 形成时间:函数执行时
- 销毁时间:函数执行完毕
当一个函数执行时,AO 对象的整个赋值形成过程分为四个步骤:
- 创建一个 AO{}对象
- 找到函数中的形参和变量,作为 AO 对象的属性名,这些属性名的属性值为 undefined
- 形参与实参相统一,即将实参的值赋值给形参
- 找到函数中的函数声明(非函数表达式),将函数名作为 AO 对象的属性名,值为函数体
比如上面 fn 函数执行时,AO 局部对象的整个赋值过程如下:
function fn(c) {
// 第一步:创建AO{}对象
/**
* AO {}
*/
// 第二步:找形参和变量
/**
AO {
c: undefined,
a: undefined,
b: undefined,
d: undefined
}
*/
// 第三步:形参与实参相统一
/**
AO {
c: true,
a: undefined,
b: undefined,
d: undefined
}
*/
// 第四步:找函数声明,函数名作为AO的属性名
/**
AO {
c: true,
a: function(){},
b: function(){},
d: undefined
}
*/
// 经过上面四个步骤,下面这一行代码执行时,AO对象如上所示
console.log(c) // true
var c = false
console.log(a) // function a(){}
var a = 1 // Tips: 这里将 AO对象中 a属性赋值为1
console.log(a) // 1
function a() {
// wo can do something
}
console.log(a) // 1
b() // 函数声明
var b = function () {
// Tips: 这里将 AO对象中的 b属性赋值为一个函数表达式
console.log('函数表达式')
}
function b() {
console.log('函数声明')
}
b() // 函数表达式
var d = 4 // Tips: 这里将 AO对象中 d属性赋值为4
}
fn(true)
当程序执行到 ‘console.log(c)’ 这一行时,AO 对象如下图所示:
留个坑后面再写......
三、什么是 VO(Variable Object)
- 含义:VO 是变量对象,是一个与执行上下文相关的特殊对象
- 包含:
- 变量 (var, 变量声明);
- 函数声明 (FunctionDeclaration, 缩写为 FD);
- 函数的形参;
- VO 与 GO/AO 的关系,如下所示:
抽象变量对象VO (变量初始化过程的一般行为)
║
╠══> 全局上下文变量对象GlobalContextVO
║ (VO === this === global)
║
╚══> 函数上下文变量对象FunctionContextVO
(VO === AO, 并且添加了<arguments>和<formal parameters>)
四、 作用域
当前正在执行的代码能够访问到变量的范围。
- 词法作用域:是在写代码或者定义的时候确定的,关注函数在何处申明
- 动态作用域:是在运行时确定的,关注函数从何处调用
js 采用词法作用域,其作用域由你在写代码是将变量和块作用域写在哪里决定的,因此当词法分析器处理代码时会保持作用域不变,
作用域链
每个函数执行都需要开辟一个私有的执行上下文,创建执行上下文会初始化作用域链。 作用域链包括当前函数存在的作用域以及所有父级作用域,当函数访问变量的时候,就会顺着这个作用域链去查找变量。
分类
- 全局作用域
- 函数作用域
- 块级作用域(es6 let const)
五、 执行上下文(EC)
简述:当前 js 代码被解析和执行时所在的环境
由 js 引擎自动创建对象,包含对应作用域中的所有变量属性
执行上下文是指:代码执行或者函数调用时 在执行栈中产生的变量对象,这个变量对象我们无法直接访问,但是可以访问其中的变量,this 对象等
执行上下文包含三个部分
- 创建变量对象(Variable object ,VO)
- 创建作用域链(scope chain)
- 确定 this 值
代码模拟执行上下文如下:
//执行上下文对象创建
ExecutionContext:{
//变量对象|活动对象(VO | AO)
[variable object | activation object]:{
arguments, //函数参数,全局执行上下文中没有arguments
variable, // 变量
funcion, // 函数声明
},
//作用域链接
scope chain:variable object + all parents scope, //变量对象和所有的父作用域
//this指向
thisValue: context object
}
类型
- 全局执行上下文:默认最基础的执行上下文,不在任何函数中的代码都位于全局上下文中,一个程序中只能存在一个全局上下文
- 函数执行上下文:每次函数调用(包括同一个函数多次调用)时,都会为该函数创建一个新的执行上下文,但是只有函数被调用时才会被创建,一个程序中可以存在多个函数执行上下文。
- eval 执行上下文
执行上下文的生命周期
一共有三个阶段:创建阶段、入栈执行阶段、出栈回收阶段
- 全局执行上下文:准备执行代码前产生,当页面关闭/刷新时销毁
- 函数执行上下文:调用函数时产生,函数执行完销毁
执行栈(Ecstack)
执行栈又叫调用栈,是用来存储 js 程序中创建的“执行上下文”的一种先进后出的结构,入栈和出栈对应的是内存的申请和释放。
当 js 代码执行的时候,首先会创建一个全局的执行上下文,并压入到当前执行栈中,之后当发生函数调用的时候,js 引擎会为该函数创建一个“函数执行上下文”并压入到栈顶,当该函数执行完毕,其对应的“执行上下文”会被移除当前执行栈,调用栈最上面的那个“执行上下文”一直处于正在执行状态。
六、 作用域和执行上下文区别
- 作用域:是静态的,代码和函数定义的时候就确定了,一旦确定就不会变化
- 执行上下文:动态的,代码和函数执行的时候创建的,代码执行完就消失
- 联系:执行上下文环境是在对应的作用域中
js 的代码在解释阶段就会确定作用域规则,因此作用域在函数定义的时候就已经确定了,而不是在函数执行的时候才确定,但是执行上下文是函数执行之前创建的,执行上下文最明显的就是 this 的指向是执行的时确定的,而作用域访问变量是编写代码的结构确定的。
七、 总结
一个作用域下
- 可能包含多个执行上下文环境(全局作用下有若干个函数被调用)
- 有可能从来没有过上下文环境(函数从来没有被调用过)
- 有可能有过(函数被调用过,上下文环境被销毁)
- 有可能同时存在一个或多个上下文环境(闭包)
- 同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量值。
作用域只是一个“地盘”,其中没有变量。变量是通过作用域对应的执行上下文环境中的变量对象来实现的。所以作用域是静态观念的,而执行上下文环境是动态上的,两者并不一样。 同一个作用域下,对同一个函数的不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值,所以,作用域中变量的值是在执行过程中确定的,而作用域是在函数创建时就确定的。 如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中找到变量的值。
引申
与通过 var 声明的有初始化值 undefined 的变量不同,通过 let/const 声明的变量直到它们的定义被执行时才初始化。在变量初始化前访问该变量会导致 ReferenceError。该变量处在一个自块顶部到初始化处理的“暂存死区”中。
使用 let / const 声明的全局变量,会被绑定到 Script 对象而不是 Window 对象,不能以 Window.xx 的形式使用;使用 var 声明的全局变量会被绑定到 Window 对象;使用 var / let / const 声明的局部变量都会被绑定到 Local 对象。注:Script 对象、Window 对象、Local 对象三者是平行并列关系。
Script 对象,如下图所示