目录
上篇文章提到作用域闭包,其中关于作用域链有说到
当一个函数定义的时候,会创建一个包含全局变量对象和 所有 包含函数(的)活动对象的 作用域链,并将它保存在这个函数对象的 [[Scope]] 内部属性上(函数也是一个对象,包含函数体和其他一些可访问或不可访问的属性,比如 name、length 等)。还记得前面说的 词法作用域 吗?
当这个函数执行的时候,会创建一个 执行上下文,将 [[Scope]] 复制到执行上下文中的作用域链。然后,创建当前的 活动对象,推到作用域链顶端。此刻,函数执行过程中可访问的作用域是:AO + Scope Chain。
注意:
- 作用域链是一个指向各级变量对象的指针列表,作用域链不清除,各级变量对象也不会清除。
- 无论子函数是否引用了外部作用域的变量,都会包含完整的作用域链,都存在内存占用问题。只是要不要把未引用外部变量的函数称作闭包,就见仁见智了,我们稍后会讨论闭包的定义问题。
关键词 English 描述 词法作用域 Lexical Scoping 作用域定义了变量的查找方法和权限,即查找路径。而词法作用域正如字面意思,它是在词法解析阶段定义的作用域,是一种静态作用域。换言之,在函数定义的时候就规定了该函数的作用域是什么。JavaScript 使用的是词法作用域。 变量对象 Variable Object A.K.A VO 一些作用域中的变量的集合。包括局部变量对象和全局变量对象。 活动对象 Activation Object A.K.A AO 当前执行作用域中的变量集合。其实就是激活的(活动的)变量对象。 作用域链 Scope Chain 函数有权访问的所有包含函数的局部变量对象(活动对象)和全局变量对象。 执行上下文 Execution Context 一段可执行代码的 VO、作用域链以及 this。即跟当前执行代码相关的所有环境上下文。 包含函数 这不是个术语,只是一种说法,指的是当前函数的父级、父级的父级…… 所有函数。
作为 JavaScript 开发者,我们必须知道 JavaScript 程序内部的执行机制。执行上下文和执行栈是JavaScript中关键概念之一,是JavaScript难点之一。 理解执行上下文和执行栈同样有助于理解其他的 JavaScript 概念如提升机制、作用域和闭包等。
1. 执行上下文
知识导图:
1.1 什么是执行上下文?
当函数被执行时,会创建一个执行上下文的对象。一个执行上下文定义了一个函数执行时的环境。
当一个函数被调用时,会创建一个活动记录(有时候也被称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。
每个函数在被定义时,就会有一个[[Scope]]属性,这个属性里包含着作用域链,而执行的前一刻都会创建一个OA对象,这个对象就是执行上下文。这个OA对象会被插入[[Scope]]中作用域的最顶端,这个对象里包含着函数体声明的所有变量、参数和方法。一个OA对象的有序列表。
简而言之,执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。
1.2 执行上下文的类型
执行上下文总共有三种类型:
- 全局执行上下文: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
- 函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论。
- Eval 函数执行上下文: 运行在 eval 函数中的代码也获得了自己的执行上下文,但由于 Javascript 开发人员不常用 eval 函数,所以在这里不再讨论。
2. 执行上下文的生命周期
执行上下文的生命周期包括三个阶段:创建阶段→执行阶段→回收阶段,本文重点介绍创建阶段。
2.1 创建阶段
当函数被调用,但未执行任何其内部代码之前,会做以下三件事:
- 创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。变量提升之前也详细复习过,需要了解跳转下面的目录4中的提升(44条消息) 【JavaScript复习】【一篇就够】作用域_码上游的博客-CSDN博客
- 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。【需要注意的是, 函数定义的时候,包含全局变量对象和 所有 包含函数(的)活动对象的 作用域链就被创建了,并将它保存在这个函数对象的 [[Scope]] 内部属性上了,此时创建的作用域链是将[[Scope]] 复制到执行上下文中的作用域链。】
- 确定this指向:包括多种情况,之前详细介绍过,需要了解跳转(44条消息) 【JavaScript复习】【一篇就够】浅析this--this绑定规则及优先级_码上游的博客-CSDN博客_this->this
在一段 JS 脚本执行之前,要先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来。变量先暂时赋值为undefined,函数则先声明好可使用。这一步做完了,然后再开始正式执行程序。
另外,一个函数在执行之前,也会创建一个函数执行上下文环境,跟全局上下文差不多,不过 函数执行上下文中会多出this arguments和函数的参数。
2.2 执行阶段
执行变量赋值、代码执行
2.3 回收阶段
执行上下文出栈等待虚拟机回收执行上下文
3. 执行上下文栈
接下来问题来了,我们写的函数多了去了,每一个函数执行时都会产生一个执行上下文,那如何管理创建的那么多执行上下文呢?
所以 JavaScript 引擎创建了执行上下文栈来管理执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
从上面的流程图,我们需要记住几个关键点:
- JavaScript执行在单线程上,所有的代码都是排队执行。
- 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
- 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
- 浏览器的JS执行引擎总是访问栈顶的执行上下文。
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
我们再来看个例子深入了解一下:
var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
上述代码运行按照如下步骤:
- 当上述代码在浏览器中加载时,JavaScript 引擎会创建一个全局执行上下文并且将它推入当前的执行栈
- 调用 changeColor函数时,此时changeColor函数内部代码还未执行,js执行引擎立即创建一个changeColor的执行上下文(简称EC),然后把这执行上下文压入到执行栈(简称ECStack)中。
- 执行changeColor函数过程中,调用swapColors函数,同样地,swapColors函数执行之前也创建了一个swapColors的执行上下文,并压入到执行栈中。
- swapColors函数执行完成,swapColors函数的执行上下文出栈,并且被销毁。
- changeColor函数执行完成,changeColor函数的执行上下文出栈,并且被销毁。
4. 思考题
我们已经了解了执行上下文栈是如何处理执行上下文的,看看下面这个问题吧:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?
答案就是执行上下文栈的变化不一样。
让我们模拟第一段代码:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
让我们模拟第二段代码:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
5. 写在最后
JavaScript复习系列已经总结到第六部分了,本系列大概有八部分,都是JavaScript中最重要的 基础内功,也是面试最高频的。
(33条消息) 【JavaScript复习】【一篇就够】浅析this--this绑定规则及优先级_码上游的博客-CSDN博客_this->this
(33条消息) 【JavaScript复习】【两篇就够】异步相关(一)_码上游的博客-CSDN博客
【JavaScript复习】【两篇就够】异步相关(二)_码上游的博客-CSDN博客
(33条消息) 【JavaScript复习】【一篇就够】作用域_码上游的博客-CSDN博客
【JavaScript复习】【一篇就够】看透闭包本质_码上游的博客-CSDN博客
(33条消息) 【JavaScript复习】事件循环知识题目全汇总_码上游的博客-CSDN博客
如果您看到了最后,不妨收藏、点赞、关注一下吧!!!
持续更新,虚心接受大佬们的批评和指点,共勉!