Javascript 中的执行环境与堆叠 曾经终败给现在 2022-07-21 11:17 135阅读 0赞 在这篇笔记中我将会深入的探讨 JS 底层中的一些观念,其中最重要的就是执行环境(Execution Context)。当您阅读完这篇文章后您可能会比较清楚关于直译器的运作方式,明白为什么有些 函式 变数 可以在他们被宣告之前就拿来使用,以及这些值是怎么决定的。 什么是执行环境? 我们说当 JS 开始执行的时候,这段程式码必须被执行在下面三种环境之一。 全域 Global:预设当您程式开始执行时的环境 函式:当我们进入一个函式 function 时的环境,也就是开始跑函式内部程式码的时候 Eval:把一串字串,当作指令来执行时的环境 也就是说一段 JS 程式码只能存在在上面这三种状态或类型。 让我们直接来看看程式码 // Global context, JS 最外层的程式码部分属于全域 var greeting = "Hi"; function person() \{ // 从大括号开始到结束进入另外一个执行环境 var \_firstName = "andy"; var \_lastName = "you"; function firstName() \{ // 另外一个执行环境 return \_firstName; \} function lastName() \{ // 执行环境 return \_lastName; \} alert(greeting + firstName() + ' ' + lastName()); \} 上面这段范例没什么特别的,我们就是有了一个全域的执行环境即 global context ,和 3 个 function context,唯一稍微要注意的是 global context 只会有一个。其他执行环境都可以存取全域的东西。 当然您可以有多个 function context 每一个 function 执行的时候就会建立一个新的 context ,OK!不管讲执行环境或者 context都好抽象,那我们就先把他们当作是一个 context 物件,那这个 context 物件讲白了就是表示一个环境,一个范围,一个状态。它会建立一个范围一个自己特有的领域,任何在 function 里面宣告的变数或其他东西都不能被外面直接存取。 如果这样还不能理解,那我们换个角度来想这件事,你把 context 当成是一张记录表格,当我开始在 global 执行程式码的时候。 任何变数,function 都会被记载 global 表 上,但是当执行到 function 内部的时候,此时会在开出另外一张 function 表 负责记录 function 内部的变数等等。 不过我个人认为 执行环境 是最贴切的翻译,当我在全域这个环境时我能够取得的变数和进到另外一个 function 环境时可能会有不一样的状况。 因此在第一小节我们就下个小结论那就是每一段 JS 在运行的时候会根据片段程式码所在的区块有其特有的 环境 执行环境的堆叠 对于执行环境有了初步的概念之后我们还得知道 - 浏览器的 JS 直译器通常是单执行绪的,意味着一次只能够做一件事。 也就是说当一个事件被执行的时候其他的任务,事件等等就会被丢到执行伫列中。这个东西我们就叫做执行堆叠 我们已经知道当 JS 开始跑的时候一开始会进入 global 执行环境,如果您在 global 环境中呼叫了一个 function A (即: A();),这个时候就会建立新的 执行环境 然后这个新的执行环境会被放到执行堆叠的最上面,同样的如果你现在在 function A 里面又叫了 function B 那么就又会在建立一个执行环境一样放到执行堆叠的最上方,浏览器永远会先处理堆叠上最上面的执行环境,一旦执行环境里面的任务都执行完了那它就会被移掉换下一个 OK 这边交代得有点乱,我们看到的程式码的时候通常最小的执行单位就是那一句一句的 statement 语句,一个语句交代了程式该做一件事。这些 statement 都会有自己的环境,也因此我们可以把环境在当作一个上层单位。一个 context 里面势必存在一些任务(语句)。就把一个 context 想像成某个任务好了。看看下面的范例可能比较有感觉 (function foo(i) \{ if (i === 3) \{ return; \} else \{ foo(++i); \} \}(0)); 这段程式码简单的呼叫自己三次每一次把参数加一,每当 foo 被呼叫的时候新的 执行环境 就被建立,然后当 执行环境 里面的程式跑完的时候,就从堆叠中把 执行环境 拿掉,把控制权交还给上一个环境一直到回到 global 为止。 关于执行环境有 5 个重点要牢记在心 单执行绪 同步执行 只有一个 global context function context 没有限制 就算是自己呼叫自己只要 call function 就会建立执行环境 详解执行环境 所以我们现在知道了每一次 call function 的时候就会建立一个新的执行环境,然而在 JS 直译器内部每次调用一个执行环境都会有两个阶段 建立阶段 当 function 被呼叫了但在开始执行内部程式码之前 建立一个 scope chain 作用域炼 建立变数,function,和参数 设定 this 的值 执行阶段 赋值,设定 function 的参考和解译执行程式码 概念上我们可以把一个 执行环境 想像成一个物件,那么这个物件大概会有三个属性如下 executionContextObject = \{ scopeChain: \{ /\* 变数物件 + 所有父代执行环境物件的变数物件\*/\}, variableObject: \{/\* 函式的参数/引数,内部的变数和函式\*/ \}, this: \{\} \} Variable Object 变数物件:根据 ECMA-262 的说明,每一个执行环境会有一个与相关连的变数物件,这个物件负责记录执行环境中定义的变数和函式。 Activation / Variable Object \[AO/VO\] 这一个执行环境物件在 function 被调用的时候建立,不过在实际的 function 被执行之前,这就是上面提到的阶段 1 - 建立阶段。在这个阶段直译器会建立 executionObject ,透过扫描函式传入的参数,内部的函式宣告,变数宣告。结果会被记录在executionObject 的 变数物件 variableObject 中。 这里我们大致模拟直译器是如何执行的流程 寻找呼叫 function 的程式码 在执行 function 之前建立 执行环境 进入 建立阶段 初始化 scope chain 建立 variable object: 建立 arguments object 检查执行环境的参数,初始化参数的名称,值以及建立参考 扫描 function 的宣告 根据找到的每一个 function 在 variable object 建立,在这边其实就是建立 function 名称在记忆体中的参考指标 如果 function 名称已经存在那么指标就会被覆写 扫描执行环境里的变数 每一个变数的宣告都会被加入 variable object 的属性中,并且初始化为 undefined,注意在这个阶段并不会赋值 如果变数名称存在就略过,继续处理下一个变数 判断决定 this 的值 执行阶段 执行程式码,赋值,一行一行跑 function foo(i) \{ var a = 'hello'; var b = function B() \{ \}; function c() \{ \} \} foo(22); 此时在建立阶段我们就会得到如下的范例 fooExecutionContext = \{ scopeChain: \{ ... \}, variableObject: \{ arguments: \{ 0: 22, length: 1 \}, i: 22, c: pointer to function c() a: undefined, b: undefined \}, this: \{ ... \} \} 如您所见,在建立阶段处理关于定义宣告的部分,此时并不会赋值,所以 function b 并没有被参考。不过参数是唯一的例外,此时参数的值已经被建立。一旦建立阶段完成,剩下的流程就是开始执行阶段,当执行阶段完成的时候执行环境就会如下 fooExecutionContext = \{ scopeChain: \{ ... \}, variableObject: \{ arguments: \{ 0: 22, length: 1 \}, i: 22, c: pointer to function c() a: 'hello', b: pointer to function B() \}, this: \{ ... \} \} 变数宣告提升 您可以找到很多关于定义 Javascript hoisting 的资料,他们通常会解释这就是一种把宣告提升到其所在区域内顶端的行为,然而这样并没有解释到细节,为什么会发生这件事,不过呢刚刚您已经知道了关于整个直译器解意的流程,现在您可以很清楚的明白为什么会这样了。 (function () \{ console.log("foo: " + typeof foo); // function pointer console.log("bar: " + typeof bar); // undefined var foo = 'hello', bar = function() \{ return 'world'; \}; function foo() \{ return 'hello'; \} \}()); 现在我们可以回答关于上面这段程式码的一些问题 为什么我们在宣告之前可以存取 foo 如果我们看看 建立阶段 的流程我们可以知道变数在这个时期早就被建立了 Foo 被宣告 2 次,为什么 foo 是 function 而不是 undefined 或 string? 即使 foo 宣告了2次,我们知道在建立阶段 function 会先被建立。因此变数已经存在了在这个阶段 string 不会被赋予 foo 因此在真正执行 function 之前 foo 是会先被建立,等他真正跑完执行阶段的时候 foo 才会被覆写成 'hello' 为什么 bar 是 undefined ? bar 就只是一个变数,在这个阶段并还没赋值所以就是 undefined 总结 下个收敛的结论就是 每一个片段程式码都会属于某个执行环境,或者说在开始执行程式码之前会先建立 执行环境 执行环境比喻来说就像是一个物件负责纪录这个 环境 下相关的事物 变数 function 等等 从上往下看这个执行环境物件最重要的是 scope chain, variable object, this 这三个属性 variable object 才是实际上记录变数,function,arguments 的地方 另外一个重要的点是 scope chain 他负责记录每个环境之间切换的关联,例如从 global -> a() 每次开始建立执行环境的时候就会分成两个阶段 开始建立执行环境的时间点是在 function 被呼叫后,实际执行内部程式码前 建立阶段,初始化这个环境,除了 arguments 外其他都只是先定义变数,函式指标,并没有赋值 执行阶段,开始一行一行执行,赋值 希望现在您可以更清楚关于 Javascript 如何运行您的程式码,了解执行环境,堆叠可以让您更清楚您的程式码在不同状态下取到的值,如此一来相信您在组织 JS 的时候会有更好的写法。 源引:https://segmentfault.com/a/1190000004491834
还没有评论,来说两句吧...