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


Posted in Javascript onFebruary 12, 2019

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

什么是执行上下文?

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

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

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

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

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

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

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

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

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

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

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

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

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

代码简单地调用自身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呢?

总结

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

Javascript 相关文章推荐
几个比较经典常用的jQuery小技巧
Mar 01 Javascript
jquery跟js初始化加载的多种方法及区别介绍
Apr 02 Javascript
select多选 multiple的使用示例
Jun 16 Javascript
jQuery中的编程范式详解
Dec 15 Javascript
angularjs基础教程
Dec 25 Javascript
JQuery中的事件及动画用法实例
Jan 26 Javascript
JS脚本根据手机浏览器类型跳转WAP手机网站(两种方式)
Aug 04 Javascript
jQuery实现浮动层随浏览器滚动条滚动的方法
Sep 22 Javascript
JS实现带动画的回到顶部效果
Dec 28 Javascript
详解Vue 全局引入bass.scss 处理方案
Mar 26 Javascript
Vue不能观察到数组length的变化
Jun 08 Javascript
如何解决.vue文件url引用文件的问题
Jan 18 Javascript
图文详解vue框架安装步骤
Feb 12 #Javascript
详解vue组件中使用路由方法
Feb 12 #Javascript
说说Vue.js中的functional函数化组件的使用
Feb 12 #Javascript
详解Vue用cmd创建项目
Feb 12 #Javascript
谈谈JavaScript中super(props)的重要性
Feb 12 #Javascript
微信小程序实现的自定义分享功能示例
Feb 12 #Javascript
图文讲解用vue-cli脚手架创建vue项目步骤
Feb 12 #Javascript
You might like
用PHP函数解决SQL injection
2006/12/09 PHP
一个漂亮的php验证码类(分享)
2013/08/06 PHP
PHP 利用Mail_MimeDecode类提取邮件信息示例
2014/01/26 PHP
PHP 面向对象程序设计(oop)学习笔记 (五) - PHP 命名空间
2014/06/12 PHP
3款值得推荐的微信开发开源框架
2014/10/28 PHP
php选择排序法实现数组排序实例分析
2015/02/16 PHP
php使用curl_init()和curl_multi_init()多线程的速度比较详解
2018/08/15 PHP
javascript之ESC(第二类混淆)
2007/05/06 Javascript
javascript中强制执行toString()具体实现
2013/04/27 Javascript
判断一个变量是数组Array类型的方法
2013/09/16 Javascript
JavaScript实现大数的运算
2014/11/24 Javascript
jQuery实现的兼容性浮动层示例
2016/08/02 Javascript
jQuery flip插件实现的翻牌效果示例【附demo源码下载】
2016/09/20 Javascript
Angularjs中的页面访问权限怎么设置
2016/11/11 Javascript
详解Angular2中Input和Output用法及示例
2017/05/21 Javascript
React通过父组件传递类名给子组件的实现方法
2017/11/13 Javascript
vue技术分享之你可能不知道的7个秘密
2018/04/09 Javascript
Vue代码整洁之去重方法整理
2019/08/06 Javascript
[01:38]DOTA2辉夜杯 欢乐的观众现场采访
2015/12/26 DOTA
[01:04:32]DOTA2-DPC中国联赛 正赛 Aster vs LBZS BO3 第二场 2月23日
2021/03/11 DOTA
python 移除字符串尾部的数字方法
2018/07/17 Python
Flask实现图片的上传、下载及展示示例代码
2018/08/03 Python
python自动化测试之如何解析excel文件
2019/06/27 Python
Python 使用 PyMysql、DBUtils 创建连接池提升性能
2019/08/14 Python
Python tkinter实现简单加法计算器代码实例
2020/05/13 Python
关于解决iframe标签嵌套问题的解决方法
2020/03/04 HTML / CSS
美国伴娘礼服商店:Evening Collective
2019/10/07 全球购物
初婚未育未抱养证明
2014/01/12 职场文书
小学生作文评语
2014/04/18 职场文书
环境卫生工作汇报材料
2014/10/28 职场文书
商超业务员岗位职责
2015/02/13 职场文书
三八节祝酒词
2015/08/11 职场文书
2016七夕情人节广告语
2016/01/28 职场文书
SpringBoot整合阿里云视频点播的过程详解
2021/12/06 Java/Android
Python集合set()使用的方法详解
2022/03/18 Python
python数字图像处理之图像自动阈值分割示例
2022/06/28 Python