【JavaScript复习】【一篇就够】JavaScript中的执行上下文,确定不看看这篇图文并茂的文章吗?

目录

1. 执行上下文

1.2 执行上下文的类型

2. 执行上下文的生命周期

2.1 创建阶段

2.2 执行阶段

2.3 回收阶段

3. 执行上下文栈

4. 思考题

5. 写在最后


        上篇文章提到作用域闭包,其中关于作用域链有说到

        当一个函数定义的时候,会创建一个包含全局变量对象和 所有 包含函数(的)活动对象的 作用域链,并将它保存在这个函数对象的 [[Scope]] 内部属性上(函数也是一个对象,包含函数体和其他一些可访问或不可访问的属性,比如 name、length 等)。还记得前面说的 词法作用域 吗?

        当这个函数执行的时候,会创建一个 执行上下文,将 [[Scope]] 复制到执行上下文中的作用域链。然后,创建当前的 活动对象,推到作用域链顶端。此刻,函数执行过程中可访问的作用域是:AO + Scope Chain。

注意:

  1. 作用域链是一个指向各级变量对象的指针列表,作用域链不清除,各级变量对象也不会清除。
  2. 无论子函数是否引用了外部作用域的变量,都会包含完整的作用域链,都存在内存占用问题。只是要不要把未引用外部变量的函数称作闭包,就见仁见智了,我们稍后会讨论闭包的定义问题。
  3. 关键词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博客

如果您看到了最后,不妨收藏、点赞、关注一下吧!!!
持续更新,虚心接受大佬们的批评和指点,共勉!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码上游

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值