您的位置:首页 > Web前端 > JavaScript

Javascript 中的执行环境与堆叠

2016-07-15 01:07 197 查看
在这篇笔记中我将会深入的探讨 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  JavaScript