深入探讨JavaScript的最基本部分之执行上下文

在这篇文章中,我将深入探讨JavaScript的最基本部分之一,即Execution Context(执行上下文)。 在本文结束时,你应该对解释器了解得更清楚:为什么在声明它们之前可以使用某些函数或变量?以及它们的值是如何确定的?

什么是执行上下文?

JavaScript的执行环境非常重要,当JavaScript代码在行时,会被预处理为以下情况之一:

  • Global code - 首次执行代码的默认环境。
  • Function code - 每当执行流程进入函数体时。
  • Eval code - 要在eval函数内执行的文本。

你可以阅读大量涉及作用域的在线资料,不过为了使事情更容易理解,让我们将术语“执行上下文”视为当前代码的运行环境或作用域。接下来让我们看一个包含global和function / local上下文的代码示例。

这里没有什么特别之处,我们有一个由紫色边框表示的全局上下文,和由绿色,蓝色和橙色边框表示的3个不同的函数上下文。 只能有1个全局上下文,可以从程序中的任何其他上下文访问。

你可以拥有任意数量的函数上下文,并且每个函数调用都会创建一个新的上下文,从而创建一个私有作用域,其中无法从当前函数作用域外直接访问函数内部声明的任何内容。 在上面的示例中,函数可以访问在其当前上下文之外声明的变量,但外部上下文无法访问在其中声明的变量或函数。 为什么会这样呢? 这段代码究竟是如何处理的?

Execution Context Stack(执行上下文堆栈)

浏览器中的JavaScript解释器被实现为单个线程。 实际上这意味着在浏览器中一次只能做一件事,其他动作或事件在所谓的执行堆栈中排队。 下图是单线程堆栈的抽象视图:

我们已经知道,当浏览器首次加载脚本时,它默认进入全局上下文执行。 如果在全局代码中调用函数,程序的顺序流进入被调用的函数,创建新的执行上下文并将其推送到执行堆栈的顶部。

如果在当前函数中调用另一个函数,则会发生同样的事情。 代码的执行流程进入内部函数,该函数创建一个新的执行上下文,该上下文被推送到现有堆栈的顶部。 浏览器将始终执行位于堆栈顶部的当前执行上下文,并且一旦函数执行完当前执行上下文后,它将从栈顶部弹出,把控制权返回到当前栈中的下一个上下文。 下面的示例显示了递归函数和程序的执行堆栈:

(function foo(i) {
  if (i === 3) {
    return;
  }
  else {
    foo(++i);
  }
}(0));

代码简单地调用自身3次,并将i的值递增1。每次调用函数foo时,都会创建一个新的执行上下文。 一旦上下文完成执行,它就会弹出堆栈并且讲控制返回到它下面的上下文,直到再次达到全局上下文。

关于执行堆栈execution stack有5个关键要点:

  1. 单线程。
  2. 同步执行。
  3. 一个全局上下文。
  4. 任意多个函数上下文。
  5. 每个函数调用都会创建一个新的执行上下文execution context,甚至是对自身的调用。

执行上下文的细节

所以我们现在知道每次调用一个函数时,都会创建一个新的执行上下文。 但是,在JavaScript解释器中,对执行上下文的每次调用都有两个阶段:

创建阶段 [调用函数时,但在执行任何代码之前]:

  1. 创建作用域链。
  2. 创建变量,函数和参数。
  3. 确定“this”的值。

激活/代码执行阶段:

  • 分配值,引用函数和解释/执行代码。

可以将每个执行上下文在概念上表示为具有3个属性的对象:

executionContextObj = {
  'scopeChain': { /* variableObject + 所有父执行上下文的variableObject */ },
  'variableObject': { /* 函数实参/形参,内部变量和函数声明 */ },
  'this': {}
}

激活对象/变量对象 [AO/VO]

在调用该函数,并且在实际执行函数之前,会创建这个executionContextObj。 这被称为第1阶段,即创造阶段。 这时解释器通过扫描函数传递的实参或形参、本地函数声明和局部变量声明来创建executionContextObj。 此扫描的结果将成为executionContextObj中的variableObject。

以下是解释器如何预处理代码的伪代码概述:

1.找一些代码来调用一个函数。

2.在执行功能代码之前,创建执行上下文。

3.进入创建阶段:

①初始化作用域链。

②创建variable object:

  • 创建arguments object,检查参数的上下文,初始化名称和值并创建引用副本。
  • 扫描上下文以获取函数声明:
  • 对于找到的每个函数,在variable object中创建一个属性,该属性是函数的确切名称,该属性存在指向内存中函数的引用指针。
  • 如果函数名已存在,则将覆盖引用指针值。
  • 扫描上下文以获取变量声明:
  • 对于找到的每个变量声明,在variable object中创建一个属性作为变量名称,并将该值初始化为undefined。
  • 如果变量名称已存在于variable object中,则不执行任何操作并继续扫描。

③确定上下文中“this”的值。

4.激活/执行阶段:

  • 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。

我们来看一个例子:

function foo(i) {
  var a = 'hello';
  var b = function privateB() {
  };
  function c() {
  }
}
foo(22);

在调用foo(22)时,创建阶段如下所示:

fooExecutionContext = {
  scopeChain: { ... },
  variableObject: {
    arguments: {
      0: 22,
      length: 1
    },
    i: 22,
    c: pointer to function c()
    a: undefined,
    b: undefined
  },
  this: { ... }
}

如你所见,创建阶段处理定义属性的名称,而不是为它们赋值,但正式的形参/实参除外。创建阶段完成后,执行流程进入函数,激活/代码执行阶段在函数执行完毕后如下所示:

fooExecutionContext = {
  scopeChain: { ... },
  variableObject: {
    arguments: {
      0: 22,
      length: 1
    },
    i: 22,
    c: pointer to function c()
    a: 'hello',
    b: pointer to function privateB()
  },
  this: { ... }
}

关于hoisting

你可以找到许多使用JavaScript定义术语hoisting的在线资源,解释变量和函数声明被hoisting到其函数范围的顶部。 但是没有人能够详细解释为什么会发生这种情况,掌握了关于解释器如何创建激活对象的新知识,很容易理解为什么。 请看下面的代码示例:

(function() {
  console.log(typeof foo); // function pointer
  console.log(typeof bar); // undefined
  var foo = 'hello',
    bar = function() {
      return 'world';
    };
  function foo() {
    return 'hello';
  }
}());

我们现在可以回答的问题是:

为什么我们可以在声明foo之前就能访问?

  • 如果我们理解了创建阶段,就知道在激活/代码执行阶段之前已经创建了变量。因此,当函数流开始执行时,已经在激活对象中定义了foo。

Foo被声明两次,为什么foo显示为function而不是undefined或string?

  • 即使foo被声明两次,我们通过创建阶段知道函数在变量之前就被创建在激活对象上了,而且如果激活对象上已经存在了属性名称,我们只是绕过了声明这一步骤。
  • 因此,首先在激活对象上创建对函数foo()的引用,并且当解释器到达var foo时,我们已经看到属性名称foo存在,因此代码不执行任何操作并继续处理。

为什么bar未定义?

  • bar实际上是一个具有函数赋值的变量,我们知道变量是在创建阶段被创建的,但它们是使用undefined值初始化的。

希望到这里你已经能够很好地掌握了JavaScript解释器如何预处理你的代码。 理解执行上下文和堆栈可以让你了解背后的原因:为什么代码预处理后的值和你预期的不一样。

你认为学习解释器的内部工作原理是多此一举还是非常必要的呢? 了解执行上下文阶段是否能够帮你你写出更好的JavaScript呢?

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。如果你想了解更多相关内容请查看下面相关链接

(0)

相关推荐

  • 深入理解JavaScript系列(11) 执行上下文(Execution Contexts)

    简介 从本章开始,我将陆续(翻译.转载.整理)http://dmitrysoshnikov.com/网站关于ECMAScript标标准理解的好文. 本章我们要讲解的是ECMAScript标准里的执行上下文和相关可执行代码的各种类型. 原始作者:Dmitry A. Soshnikov 原始发布: 2009-06-26 俄文原文:http://dmitrysoshnikov.com/ecmascript/ru-chapter-1-execution-contexts/ 英文翻译:Dmitry A.

  • 一篇文章弄懂javascript中的执行栈与执行上下文

    前言 作为一个前端开发人员,弄清楚JavaScript的执行上下文有助于我们理解js中一些晦涩的概念,比如闭包,作用域,变量提升等等. 执行栈 执行栈用于存储代码执行期间创建的所有执行上下文.具有FILO接口,也被称为调用栈. 当JavaScript代码被运行的时候,会创建一个全局上下文,并push到当前执行栈.之后当发生函数调用的时候,引擎会为函数创建一个函数执行上下文并push到栈顶.引擎会先执行调用栈顶部的函数,当函数执行完成后,当前函数的执行上下文会被移除当前执行栈.并移动到下一个上下文

  • 跟我学习javascript的执行上下文

    在这篇文章里,我将深入研究JavaScript中最基本的部分--执行上下文(execution context).读完本文后,你应该清楚了解释器做了什么,为什么函数和变量能在声明前使用以及他们的值是如何决定的. 1.EC-执行环境或者执行上下文 每当控制器到达ECMAScript可执行代码的时候,控制器就进入了一个执行上下文(好高大上的概念啊). javascript中,EC分为三种: 全局级别的代码 –– 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境. 函数级别的代码

  • 对于Javascript 执行上下文的全面了解

    在这篇文章中,将比较深入地阐述下执行上下文 – JavaScript中最基础也是最重要的一个概念.相信读完这篇文章后,你就会明白javascript引擎内部在执行代码以前到底做了些什么,为什么某些函数以及变量在没有被声明以前就可以被使用,以及它们的最终的值是怎样被定义的. 什么是执行上下文 Javascript中代码的运行环境分为以下三种: 全局级别的代码 – 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境. 函数级别的代码 – 当执行一个函数时,运行函数体中的代码. Ev

  • 详释JavaScript执行环境与执行栈

    执行环境 执行环境 ( 也称"执行上下文" ) 可以说是 JavaScript 最重要的一个概念.那么执行环境到底是什么呢?一句话就可以概括:代码 ( 包括函数 ) 执行时所需要的所有信息就是执行环境.由于 ES 历经多个版本,所以执行环境的标准也一直在变,下面列出了三个主要的版本内容: ES3 标准中的执行环境 scope:作用域,如果有作用域嵌套的情况就称作"作用域链". variable object:变量对象,用于存储标识符的特殊对象. this value

  • js bind 函数 使用闭包保存执行上下文

    复制代码 代码如下: window.name = "the window object" function scopeTest() { return this.name; } // calling the function in global scope: scopeTest() // -> "the window object" var foo = { name: "the foo object!", otherScopeTest: fu

  • JS ES6中setTimeout函数的执行上下文示例

    本文介绍的是setTimeout函数,延迟执行函数里的执行上下文,分享给大家供大家参考学习,来看看详细的内容: (1)ES5中,setTimeout里面的函数的执行上下文为全局上下文,举例来说: function log(){ setTimeout(function(){console.log(this.id)},100) } var id=42 log.call({id:21}) //输出的结果为42 我们可以看到setTimeout,被延迟执行的函数,里面的this,指向的是全局作用域,也就

  • 浅析JavaScript作用域链、执行上下文与闭包

    闭包和作用域链是JavaScript中比较重要的概念,这两天翻阅了一些资料,把相关知识点给大家总结了以下. JavaScript 采用词法作用域(lexical scoping),函数执行依赖的变量作用域是由函数定义的时候决定,而不是函数执行的时候决定.以下面的代码片段举例说明,通常来说(基于栈的实现,如 C 语言) foo 被调用之后函数内的本地变量 scope 会被释放,但是从词法上看 foo 的内嵌匿名函数中 scope 应该指的是 foo 的本地变量 scope ,并且实际上代码的运行结

  • 深入理解JavaScript 中的执行上下文和执行栈

    如果你是或者想成为一名 JavaScript 开发者,你必须知道 JavaScript 程序内部是如何执行的.理解执行上下文和执行栈对于理解其他 JavaScript 概念(如变量声明提升,作用域和闭包)至关重要. 正确理解执行上下文和执行栈的概念将使您成为更出色的 JavaScript 开发者. 闲话少说,让我们开始吧 :) 分享自 Bit的博客 使用 Bit 应用所提供的组件作为构建模块,你就是架构师.随时随地和你的团队分享.发现和开发组件,快来尝试鲜! Bit - 分享和创造代码组件: B

  • 简单了解JavaScript中的执行上下文和堆栈

    什么是执行上下文? JavaScript的执行环境非常重要,当JavaScript代码在行时,会被预处理为以下情况之一: Global code - 首次执行代码的默认环境. Function code - 每当执行流程进入函数体时. Eval code - 要在eval函数内执行的文本. 你可以阅读大量涉及作用域的在线资料,不过为了使事情更容易理解,让我们将术语"执行上下文"视为当前代码的运行环境或作用域.接下来让我们看一个包含global和function / local上下文的代

随机推荐