原理与经典思考题全解析)
技术博客文档教程【免费下载链接】Blog冴羽写博客的地方预计写四个系列JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。项目地址https://gitcode.com/GitHub_Trending/blo/Blog点击查看免费下载本文是冴羽博客「JavaScript 深入系列」的第三篇围绕JavaScript 引擎如何管理执行上下文这一核心机制展开先破除JavaScript 代码总是顺序执行的直观印象引出可执行代码与执行上下文的概念再用数组模型模拟执行上下文栈ECStack的压栈、弹栈全过程并以此解答上篇《词法作用域和动态作用域》留下的经典思考题。读完本文你将掌握执行上下文栈的工作模型、globalContext常驻栈底的特性以及函数调用即压栈、函数返回即弹栈的底层规律并能用同一套模型解释复杂的嵌套调用与闭包场景。顺序执行——两个例子颠覆直觉谈到 JavaScript 的代码执行顺序绝大多数开发者都有顺序执行的直观印象。例如下面这段代码按照书写顺序依次执行结果符合预期var foo function () { console.log(foo1); } foo(); // foo1 var foo function () { console.log(foo2); } foo(); // foo2但换一种写法结果却完全不同function foo() { console.log(foo1); } foo(); // foo2 function foo() { console.log(foo2); } foo(); // foo2两处foo()打印的都是foo2。刷过面试题的开发者都知道原因JavaScript 引擎并不是一行一行地分析和执行程序而是一段一段地分析执行。当执行一段代码的时候会进行一个准备工作——第一个例子对应变量提升var foo被提升但值未赋值第二个例子对应函数提升后声明的函数覆盖了先声明的函数。但本文真正想让大家思考的是这个一段一段中的段究竟是怎么划分的JavaScript 引擎遇到一段怎样的代码时才会做准备工作可执行代码Executable Code的三种类型答案要从 JavaScript 的可执行代码executable code类型说起。JavaScript 中的可执行代码其实只有三种全局代码global code整个脚本文件或script标签内的顶层代码函数代码function code每次调用函数时函数体内的代码eval 代码eval code通过eval()动态执行的代码。当执行到其中任何一种可执行代码时引擎就会进行准备工作这个准备工作在专业术语中被称为创建执行上下文execution context。说明虽然现代 JavaScript 还存在Module代码模块顶层代码等更细的分类但本文所述三种类型是理解执行上下文栈最基础的划分也是本系列词法作用域和动态作用域讨论的前提。执行上下文栈ECS如何管理无数个执行上下文函数多了、调用嵌套深了创建的执行上下文自然也多JavaScript 引擎如何管理它们答案是执行上下文栈Execution context stack简称 ECS。为了模拟执行上下文栈的行为可以把执行上下文栈定义为一个数组ECStack [];当 JavaScript 开始解释执行代码时最先遇到的就是全局代码所以初始化时首先向执行上下文栈压入一个全局执行上下文用globalContext表示ECStack [ globalContext ];这里有一个关键特性只有当整个应用程序结束的时候ECStack 才会被清空所以 ECStack 的底部永远有一个globalContext。无论嵌套多少层函数调用全局执行上下文始终存在。压栈与弹栈的完整流程当执行一个函数时就会创建一个执行上下文并压入执行上下文栈当函数执行完毕其执行上下文从栈中弹出。以经典的嵌套调用为例function fun3() { console.log(fun3) } function fun2() { fun3(); } function fun1() { fun2(); } fun1();整个执行过程用伪代码描述如下// 伪代码 // fun1() ECStack.push(fun1 functionContext); // fun1中竟然调用了fun2还要创建fun2的执行上下文 ECStack.push(fun2 functionContext); // 擦fun2还调用了fun3 ECStack.push(fun3 functionContext); // fun3执行完毕 ECStack.pop(); // fun2执行完毕 ECStack.pop(); // fun1执行完毕 ECStack.pop(); // javascript接着执行下面的代码但是ECStack底层永远有个globalContext可以直观地看到函数调用越深栈越高每次pop()之后控制权交还给栈顶之下的执行上下文继续执行其剩余代码。这种后进先出LIFO的模型正是理解 JavaScript 中递归、异常抛出throw/try...catch以及闭包行为的基础。解答思考题两段代码的执行上下文栈变化差异现在我们已经了解执行上下文栈如何处理执行上下文可以回答上一篇《JavaScript 深入之词法作用域和动态作用域》最后提出的思考题了。题目出自《JavaScript 权威指南》第一段代码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()();两段代码的执行结果相同都打印local scope因为 JavaScript 采用词法作用域函数f的作用域基于其定义位置与调用方式无关但两段代码究竟有什么不同答案就是执行上下文栈的变化不一样。模拟第一段代码return f()先调用后返回ECStack.push(checkscope functionContext); ECStack.push(f functionContext); ECStack.pop(); ECStack.pop();模拟第二段代码return f;后由外层立即调用fECStack.push(checkscope functionContext); ECStack.pop(); ECStack.push(f functionContext); ECStack.pop();区别一目了然第一段f是在checkscope的执行上下文中被压栈、弹栈的f弹出后checkscope才弹出两者是嵌套关系第二段checkscope先执行完毕并弹出之后f才作为返回值被外部调用重新压入栈中两者是先后关系。这也是为什么return f的场景下f能够访问checkscope内的scope变量——虽然checkscope的执行上下文已从栈中弹出但f在创建时通过内部属性[[scope]]保存了父级作用域链词法作用域在函数定义时已确定这一机制正是闭包的基础本系列后续文章会详细展开。系列呼应从仓库文档看执行上下文栈的完整图景执行上下文栈只是 JavaScript 执行模型的一环。在本仓库「深入系列文章」中围绕这一主题形成了一条完整的知识链建议对照阅读《JavaScript深入之词法作用域和动态作用域》本文思考题的出处讲解 JavaScript 采用词法作用域静态作用域函数作用域在定义时决定文中还提到动态作用域语言 bash 的对比实验其可运行脚本保存在 demos/scope/scope.bash《JavaScript深入之变量对象》讲解执行上下文中的变量对象VO与活动对象AO说明进入执行上下文阶段如何加入形参、函数声明、变量声明《JavaScript深入之作用域链》讲解函数创建时保存[[scope]]、激活时构建Scope [AO].concat([[Scope]])的完整过程并以checkscope为例演示 ECStack 的压栈与弹栈《JavaScript深入之执行上下文》把执行上下文栈、变量对象、作用域链三者结合起来对本文的思考题第一段代码做了逐步推演从globalContext入栈、checkscope.[[scope]]保存到checkscopeContext、fContext依次压栈与弹栈并留下第二段代码供读者自行模拟。从源码结构看每个执行上下文都包含三个重要属性——变量对象Variable objectVO、作用域链Scope chain与this本文所讲的执行上下文栈正是这三个属性赖以存在与切换的容器。理解了压栈/弹栈的时机才能真正理解进入执行上下文时变量对象如何初始化、作用域链如何拼接。小结本文的核心结论可以归纳为四点JavaScript 引擎分段执行代码三种可执行代码全局、函数、eval各自对应一个执行上下文执行上下文由执行上下文栈ECStack管理全局执行上下文globalContext常驻栈底应用结束前永不清空函数调用时其执行上下文压栈函数执行完毕弹栈栈顶始终是当前正在执行的上下文上篇思考题中两段结果相同的代码区别在于执行上下文栈的变化不同一段是f在checkscope内部嵌套压栈一段是checkscope先弹栈、f再被压入。当然执行上下文栈的变化不同仍是相对概括的回答。要更详细地剖析两个函数执行上的区别如变量对象如何初始化、作用域链如何拼接、this如何确定需要进一步探究执行上下文内部包含的内容——这正是下一篇《JavaScript 深入之变量对象》的主题见 JavaScript深入之变量对象。如果你希望系统性补齐这些 JavaScript 底层概念可以按本仓库 README.md 中的「深入系列」目录顺序阅读原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call/apply/bind、new、继承等难点均有对应文章展开。赞分享技术博客文档教程【免费下载链接】Blog冴羽写博客的地方预计写四个系列JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。项目地址https://gitcode.com/GitHub_Trending/blo/Blog点击查看免费下载相关推荐AVA 执行上下文Execution Context完全指南深入理解每个测试与钩子收到的 t 对象AVA 执行上下文Execution Context完全指南深入理解每个测试与钩子收到的 t 对象 AVA 是面向 Node.js 的并发测试运行器它把测试深入解析 truffle/require在 Truffle 上下文中安全执行 JavaScript 与 TypeScript 模块深入解析 truffle/require在 Truffle 上下文中安全执行 JavaScript 与 TypeScript 模块 导读 truffle/区块链开发工具Web3深入解析 Flask 的 App Context 与 Request Context上下文机制与源码级原理深入解析 Flask 的 App Context 与 Request Context上下文机制与源码级原理 本篇指南以 Flask 官方的 App/Reque后端Web框架上一篇FastAPI分块上传存储对象存储集成完整指南下一篇minimind-v模型转换指南使用convert_vlm.py进行格式转换创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考