JS作用域和执行上下文怎么理解?
作用域
作用域(scope),一个独立的区域,它决定了当前执行代码对变量的访问权限,即作用域最大的用处就是隔离变量,不同作用域下的同名的变量不会发生冲突。
作用域类型
全局作用域和局部作用域
- 在全局作用域下定义的变量称之为全局变量,以下
outVariable
和outFun
为全局变量 - 在局部作用域下定义的变量称之为局部变量,以下
inVariable
和innerFun
为局部变量 - 未定义而直接赋值的变量将自动声明为全局变量,以下
variable
为全局变量(隐式声明全局变量,就是不使用var声明,直接进行赋值的变量,在不严格模式中,相当于window.变量这种方式,但在严格模式下,会报错) - 在web中Window是全局对象,全局对象的属性就是全局变量
- 内层作用域可以访问外层作用域变量,即在
innerFun
函数作用域中可以访问inVariable
和outVariable
变量 - 外层作用域不能访问内层作用域的变量,即
innerFun
函数不能在外层作用域下调用/访问
var outVariable = "我是全局变量"
window.name='我是全局变量2'
function outFun(){ // 外层函数
var inVariable = "我是局部变量";
function innerFun() { //内层函数
console.log(inVariable); // 我是局部变量
console.log(outVariable); // 我是全局变量
}
variable = "我是未定义直接赋值的变量";
}
console.log(outVariable); // 我是全局变量
console.log(variable); //我是未定义直接赋值的变量
console.log(window.name); // 我是全局变量2
outFun() // 正常执行
innerFun() //报错
作用域链
假设要获取某变量a,会先在当前作用域寻找变量a,如果当前作用域中定义了变量a,则得到此变量;如果当前作用域未定义变量a,则向父级作用域一层一层地向外查找,找到则返回变量,如果全局作用域里还没有,就返回undefined
;类似于顺着一条链条从里往外一层一层查找变量,这条链条,我们就称之为作用域链。
等下了解了执行上下文,就能更进一步了解作用域链查找,了解词法环境中的环境记录和对外部引用之后就知道为什么可以一层一层往上找。
执行上下文
执行上下文(Execution context stack),一个解析和执行代码的环境;即代码都在执行上下文中运行。是一个抽象的概念。
执行上下文类型
全局执行上下文、函数执行上下文、Eval函数执行上下文
- 全局执行上下文-基础的上下文、任何不在函数内部的代码都在全局上下文中(一个程序中只会有一个全局执行上下文)
- 函数执行上下文-当一个函数被调用时, 都会为该函数创建一个新的执行上下文(任意多个)
- Eval函数执行上下文-
eval
函数内部的代码也会有它属于自己的执行上下文(任意多个)
执行上下文生命周期
创建阶段、执行阶段、回收阶段
创建阶段
-
决定this的值
- 在全局执行上下文中,
this
的值指向全局对象。(在浏览器中,this
引用Window
对象)。 - 在函数执行上下文中,
this
的值取决于该函数是如何被调用的。
- 在全局执行上下文中,
-
创建词法环境组件(LexicalEnvironment component)
-
是一种包含 标识符 => 变量 隐射关系的一种结构
-
词法环境有两个组成部分
- 环境记录(EnvironmentRecord):储存变量和函数声明的实际位置
- 对外部环境的引用(Outer):当前可以访问的外部词法环境
-
词法环境的两种类型
- 全局坏境:没有外部环境的引用,拥有一个全局对象window和关联的方法和属性: Math,String,Date等。还有用户定义的全局变量,并将this指向全局对象。
- 函数环境:用户在函数定义的变量将储存在环境记录中。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。环境记录中包含。用户声明的变量。函数。还有arguments对象。
-
-
创建变量环境组件(VariableEnvironment component)
- 变量环境也是一个词法环境
- 词法环境和变量环境的区别在于前者用于存储函数声明和变量let 和 const 绑定,而后者仅用于存储变量 var 绑定
看个例子,看看这样的代码会生成怎么样的词法环境和变量环境
let a = 20;
const b = 30;
var c;
function add(e, f) {
var g = 20;
function c(){}
return e + f + g;
}
c = add(20, 30);
全局环境
GlobalExectionContent = { // 全局执行上下文
LexicalEnvironment: { // 词法环境--用于存储函数声明和变量let 和 const 绑定
EnvironmentRecord: { //第一部分:环境记录
Type: "Object",
a: <uninitialied>,
b: <uninitialied>,
add: <func>
// 剩余标识符
},
Outer: null, // 第二部分:对外部环境的引用
},
VariableEnvironment: { // 变量环境--仅用于存储变量 var 绑定
EnvironmentRecord: { //第一部分:环境记录
Type: "Object",
c: undefined,
// ....
},
Outer: null, // 第二部分:对外部环境的引用
}
}
从上我们可以知道:使用let和const声明的变量在词法环境创建时是未赋值初始值。而使用var定义的变量在变量环境创建时赋值为undefined。这也就是为什么const、let声明的变量在声明钱调用会报错,而var声明的变量不会
函数环境
FunctionExectionContent = { // 函数执行上下文
LexicalEnvironment: { // 词法环境--用于存储函数声明和变量let 和 const 绑定
EnvironmentRecord: { //第一部分:环境记录
Type: "Declarative",
arguments: {
0: 20,
1: 30,
length: 2,
},
e: 20,
f: 30,
c: reference to function c(){}
// ....
},
Outer: GlobalLexicalEnvironment, // 第二部分:对外部环境的引用
},
VariableEnvironment: { // 变量环境--仅用于存储变量 var 绑定
EnvironmentRecord: { //第一部分:环境记录
Type: "Declarative",
g: undefined,
// 剩余标识符
},
Outer: GlobalLexicalEnvironment, // 第二部分:对外部环境的引用
}
}
执行阶段
执行变量赋值、代码执行
注:在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let
变量的值,它会被赋值为 undefined
。
回收阶段
执行上下文出栈等待虚拟机回收执行上下文
执行上下文栈
执行上下文栈(Execution Context Stack),也叫调用栈或执行栈;栈是一种后进先出的数据结构;简单来说就是执行代码的地方。
JavaScript引擎在是怎么样执行我们的代码呢,大致如下:
- 创建全局执行上下文并入栈
- 遇到函数调用,创建新的函数执行上下文并入栈(每遇到函数调用就创建执行上下文入栈)
- 当该函数执行结束,该函数上下文从栈中弹出,js引擎处理栈中的下一个上下文
举个例子
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
- JS引擎创建全局执行上下文入栈
- 遇到
first()
调用,创建first的函数执行上下文入栈,控制流程转到该执行上下文 - 执行first,遇到
second()
调用,创建second的函数执行上下文入栈,控制流程转到该执行上下文 - 执行second函数,执行完毕,second的函数执行上下文出栈,控制流程转到first函数执行上下文
- first函数继续执行,执行完毕,first的函数执行上下文出栈,控制流程回到全局执行上下文
- 所有代码执行完毕,JS引擎从当前栈中移除全局执行上下文
如图
作用域和执行上下文的区别:作用域在函数定义时就已经确定了,并且不会改变;执行上下文是在函数调用的时候确定,随时可能改变;
全局对象、活动对象、变量对象
在学习的过程中你可能会听到这几个词,全局对象、活动对象、变量对象,那他们分别是什么意思呢
全局对象(Global Object)
JS在执行的时候会先在堆中开辟一个空间来加载基础的GO对象(存放可被JS直接调用的变量和方法,比方window)接下来会对当前的JS文件进行parse,解析后会对GO对象进行补充(向里面添加变量和函数名);也就是在创建全局上下文的时候会创建全局对象,将this指向这全局对象;
活动对象(Activive Object )
在解析函数时,会创建一个Activation Objec(AO)
变量对象(Variable Object)
当代码解析完后便会开始执行,创建全局执行上下文(GEC)放入调用栈中,其中的VO指向GO;函数调用时创建函数执行上下文(FEC)放入调用栈中,其中的VO指向AO;执行完后会开始进行内存清理,如果某个内存块没有被引用或者从GO通过引用箭头达不到便会被删除;
是不是和前面介绍的执行上下文创建有点不一样,因为这是早期ECMA的版本规范,在ES5+之后的执行上下文变成了This Binding(this绑定)、LexicalEnvironment(词法环境)、VariableEnvironment(变量环境);可是我看完还是一脸懵逼,那按上面的描述画个图看看;
之前大概就是这样子的。
在早期ECMA的版本规范中:每一个执行上下文会被关联到一个变量环境(Variable Object,简称VO) ,在源代码中的变量和函数声明会被作为属性添加到VO中。对应函数来说,参数也会被添加到VO中。
- 也就是上面所创建的GO或者AO都会被关联到变量环境(VO)上,可以通过VO查找到需要的属性;
- 规定了VO为Object类型,上文所提到的GO和AO都是Object类型;
在最新ECMA的版本规范中(ES5+):每一个执行上下文会关联一个变量环境(Variable Environment,简称VE) ,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中。
- 也就是相比于早期的版本规范,对于变量环境,已经去除了VO这个概念,提出了一个新的概念VE;
- 没有规定VE必须为Object,不同的JS引擎可以使用不同的类型,作为一条环境记录添加进去即可;
- 虽然新版本规范将变量环境改成了VE,但是JavaScript的执行过程还是不变的,只是关联的变量环境不同,将VE看成VO即可;
最后做道题
function fn(a, c) {
console.log(a);
var a = 123
console.log(a);
console.log(c);
function a() { }
if (false) {
var d = 678
}
console.log(d);
console.log(b);
var b = function () { }
console.log(b);
function c() { }
console.log(c);
}
fn(1, 2)
如果做错了,可以看看预编译的过程;
- 创建 AO — Activation Object 对象,即执行期上下文
- 寻找形参和变量声明,将变量和形参都作为 AO 的属性名,值设为 undefined
- 形参实参相统一
- 在函数体中寻找函数声明,赋给函数体
转载自:https://juejin.cn/post/7209698674905219133