您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > javascript

JavaScript中执行上下文和执行栈

时间:2019-08-06 09:24:25  来源:  作者:

本文将深入探讨JAVAScript中最重要的基础知识之一:执行上下文。通过对此篇文章的阅读,对以下几个方面的知识你将会有更加清晰的认识:

  • 解释器的执行机制
  • 为何函数和变量可以在声明前使用以及它们的值究竟是如何确定的

什么是执行上下文?

当代码在JS中运行时,代码的执行环境非常重要,JavaScript中可执行的代码分为以下几类:

  • 全局代码:代码首次执行时所进入的默认执行环境
  • 函数代码:函数体内的代码
  • Eval代码:eval内部的代码

我们可以在网上找到很多与作用域相关的文档等,本文为了便于知识点的理解,将执行上下文看作是当前代码执行所处的环境/作用域。下面是一个包括全局和函数上下文的代码示例:

(译文)JavaScript中执行上下文和执行栈

 

以上示例代码结构很简明,一个由紫色实线包裹的全局上下文和三个分别由绿色、蓝色和橙色实线包裹的函数上下文。每个程序中只能有一个可被其他程序所访问的全局上下文。函数上下文可以有任意多个,并且每个函数在调用的时候都会产生一个新的函数上下文和一个私有的作用域,当前作用域中所声明的任何变量都不能被外部所直接访问或调用。上例中,函数可直接访问当前上下文外部声明的变量,但是外部函数上下文不能访问内部声明的变量或者函数。为何会出现这种情况呢?代码到底是怎么执行的呢?

执行环境栈

浏览器中JavaScript解释器的运行是单线程的。这也就意味着在浏览器中同一时刻只能做一件事情,其他行为或者事件需要在执行栈中排队等待。下图是对单线程的抽象展示:

(译文)JavaScript中执行上下文和执行栈

 

当浏览器首次加载脚本语言的时候,会默认进入全局执行上下文。如果在全局代码中调用其他函数,当前程序的时序会自动进入所调用的函数中,与此同时会创建一个新的执行上下文并将其压入执行栈的顶部。如果在当前函数内部调用其他函数,执行过程如上所述。代码的执行流程会进入到内部函数中,创建一个新的执行上下文并将它压入执行栈的顶部。浏览器永远执行位于栈顶的执行上下文,并且一旦当前函数执行上下文执行结束,它将从栈顶弹出,执行控制权也会回到当前栈的新栈顶。这样,执行环境栈中的上下文就会被依次执行和弹出栈顶,直到回到全局上下文,下例所示:

(function foo(i){
 if (i===3) {
 return;
 }else{
 foo(++i);
 }
}(0))
复制代码

代码自调用三次,i的值不断从1自增。每次函数foo被调用的时候,一个新的执行上下文就会创建。一旦当前上下文执行结束,它就会从栈顶弹出,回到栈顶的新的上下文,直到再次回到全局上下文。

(译文)JavaScript中执行上下文和执行栈

 

执行栈中需要记住的5个关键点:

  • 单线程
  • 同步执行
  • 唯一的一个全局上下文
  • 不限个数的函数上下文
  • 每个函数的调用都会产生一个新的执行上下文,即使是函数对自己的调用

详解执行上下文

截至目前我们已经知道每当一个函数被调用的时候,就会产生一个新的执行上下文。但是,在JavaScript解释器中,对每个执行上下文的调用都分为以下两个阶段:

  • 创建阶段[当函数被调用,内部代码被执行之前的阶段]:
  • 创建作用域链
  • 创建变量、函数、参数
  • 确定this的值
  • 激活/代码执行阶段:
  • 确定函数的值和引用,然后执行代码

因为可以将执行上下文概念性的描述为含有三个属性的对象:

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

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

执行上下文对象是在函数被调用,但是在函数被执行前所产生的。也就是上文所述的阶段1—创建阶段。比部分中,解释器对执行上下文对象的创建主要是通过浏览函数的实参和形参、当前函数内部的变量声明和函数声明。这部分的浏览结果会成为执行上下文对象中的变量对象。

解释器对代码执行的伪逻辑概述:

  • 查找函数调用的代码
  • 在执行代码前,创建执行上下文
  • 进入创建上下文阶段:
  • 初始化作用域链
  • 创建变量对象:
  • 创建参数对象,检查上下文中的参数,初始化参数名称和值并创建引用副本
  • 浏览上下文中的函数声明:
  • 每找到一个函数,就在变量对象中添加一个新的属性,该属性命名为当前函数名,指向函数在内存中的引用
  • 如果函数名已经存在,所对应的属性值将被重写,指向新的函数引用
  • 浏览上下文中的变量声明:
  • 每找到一个变量声明,在变量对象中添加一个新的属性,该属性命名为当前变量名,并给该属性赋值为undefined
  • 如果变量名已经在变量对象中存在,将不进行任何操作,继续浏览当前上下文
  • 确定上下文中this的指向
  • 代码执行阶段:
  • 分配变量值并且逐行执行当前上下文中的代码

下面看一个例子:

function foo(i){
 var a = 'hello',
 var b = function privateB(){
 
 },
 function c(){
 
 }
}
foo(22);
复制代码

当调用函数foo的时候,创建阶段如下所示:

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':{...}
}
复制代码

变量提升

网上很多关于JavaScript中变量提升的定义,定义中指出变量和函数的声明会被提升至当前函数作用域的顶部。但是,并没有解释为什么会存在变量提升以及解释器如何创建激活对象,其实原因很简单,以下面的代码为例:

(function() {
 console.log(typeof foo); // function pointer
 console.log(typeof bar); // undefiend
 
 var foo = 'hello',
 bar = function (){
 return 'world';
 };
 
 function foo(){
 return 'hello';
 };
}())
复制代码

对于疑问和解答如下:

  • 为什么我们可以在声明foo前访问它?
  • 回顾创建阶段,变量在函数执行前已经被创建。因此在函数执行前,foo已经在激活对象中创建。
  • foo被声明了两次,为什么foo的类型是function而不是undefined或者string?
  • 尽管foo被声明两次,在创建阶段中,函数先于变量在激活对象中创建,并且如果激活对象中已经存在属性名,则不会影响已经存在的属性。
  • 所以,对于函数foo的引用首先在激活对象中已经创建,并且当解释器到达var foo语句,解释器发现在变量对象中foo已经被创建,因此就会跳过然后继续后续操作。
  • 为什么bar的值是undefined?
  • bar实际上是一个值为函数的变量,在创建阶段变量会被初始化为undefined 。

注:以上部分译自此文,如有侵权请告知;如有翻译不妥,还请各位读者指正。以下是我对本文知识点的简要总结。

简要总结

  • 每个函数被调用的时候,都会创建一个新的执行上下文,并将当前执行上下文压入栈顶
  • 每个执行上下文可以看作是具有以下3个属性的对象:
  • 作用域链
  • 变量对象/激活对象(VO/AO)
  • this
  • 每个执行上下文的建立分为两个阶段:创建阶段和执行阶段
  • 执行上下文创建阶段,变量对象VO初始化的先后顺序:函数参数、函数声明、变量声明。关于此部分两个常见问题的解答如下:
  • 1、"函数声明过程中,变量对象中如果已存在同名的属性,则替换它的值"这句话如何理解?以下述代码为例:
function foo(i){
 console.log(i); // function pointer
 var i = function (){
 
 }
}
foo(2);
复制代码
变量对象初始化第一步:函数参数
复制代码
executionContextObj = {
 'scopeChain':{...},
 'variableObject':{
 arguments:{
 0:2,
 length: 1,
 },
 i:2
 }
}
复制代码
变量对象初始化第二步:函数声明
 函数声明过程中,变量对象中已存在同名的属性i,将其值由"1"替换为新值"function"
复制代码
executionContextObj = {
 'scopeChain':{...},
 'variableObject':{
 arguments:{
 0:2,
 length: 1,
 },
 i: function (){
 
 }
 }
}
复制代码
  • 2、"变量声明过程中,变量对象中如果已存在同名的属性,则不进行任何操作"这句话如何理解?以下述代码为例:
 function foo(i){
 console.log(i); // function pointer
 var i = function (){
 
 },
 var i = 9;
 }
 foo(2);
复制代码
 变量对象初始化第一步:函数参数
复制代码
executionContextObj = {
 'scopeChain':{...},
 'variableObject':{
 arguments:{
 0:2,
 length: 1,
 },
 i:2
 }
}
复制代码
 变量对象初始化第二步:函数声明
 函数声明过程中,变量对象中已存在同名的属性i,将其值由‘1’替换为新值‘function’
复制代码
executionContextObj = {
 'scopeChain':{...},
 'variableObject':{
 arguments:{
 0:2,
 length: 1,
 },
 i: function (){
 
 }
 }
}
复制代码
 变量对象初始化第三步:变量声明
 变量声明过程中,变量对象中已存在同名的属性i,不进行任何操作。
复制代码
executionContextObj = {
 'scopeChain':{...},
 'variableObject':{
 arguments:{
 0:2,
 length: 1,
 },
 i: function (){
 
 }
 },
 'this':{...}
}
复制代码


Tags:JavaScript   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
1、通过条件判断给变量赋值布尔值的正确姿势// badif (a === 'a') { b = true} else { b = false}// goodb = a === 'a'2、在if中判断数组长度不为零...【详细内容】
2021-12-24  Tags: JavaScript  点击:(6)  评论:(0)  加入收藏
给新手朋友分享我收藏的前端必备javascript已经写好的封装好的方法函数,直接可用。方法函数总计:41个;以下给大家介绍有35个,需要整体文档的朋友私信我,1、输入一个值,将其返回数...【详细内容】
2021-12-15  Tags: JavaScript  点击:(20)  评论:(0)  加入收藏
作者:一川来源:前端万有引力 1 写在前面Javascript中的apply、call、bind方法是前端代码开发中相当重要的概念,并且与this的指向密切相关。本篇文章我们将深入探讨这个关键词的...【详细内容】
2021-12-06  Tags: JavaScript  点击:(19)  评论:(0)  加入收藏
概述DOM全称Document Object Model,即文档对象模型。是HTML和XML文档的编程接口,DOM将文档(HTML或XML)描绘成一个多节点构成的结构。使用JavaScript可以改变文档的结构、样式和...【详细内容】
2021-11-16  Tags: JavaScript  点击:(35)  评论:(0)  加入收藏
一、判断是否IE浏览器(支持判断IE11与edge)function IEVersion() {var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串var isIE = userAgent.indexOf("comp...【详细内容】
2021-11-02  Tags: JavaScript  点击:(40)  评论:(0)  加入收藏
Null、Undefined、空检查普通写法: if (username1 !== null || username1 !== undefined || username1 !== '') { let username = username1; }优化后...【详细内容】
2021-10-28  Tags: JavaScript  点击:(51)  评论:(0)  加入收藏
1、前言async函数,也就是我们常说的async/await,是在ES2017(ES8)引入的新特性,主要目的是为了简化使用基于Promise的API时所需的语法。async和await关键字让我们可以用一种更简...【详细内容】
2021-09-17  Tags: JavaScript  点击:(61)  评论:(0)  加入收藏
为什么要使用 debugger这篇文章将介绍如何使用断点来进行 JavaScript 调试。在读这篇文章之前,需要问一个问题:为什么要使用断点来进行调试?我们首先需要认可使用断点的是必要...【详细内容】
2021-08-26  Tags: JavaScript  点击:(66)  评论:(0)  加入收藏
JavaScript 可以做很多好玩的事, 从复杂的框架到处理API,有太多的东西需要学习。但是,它也能让我们只用一行就能做一些了不起的事情。1. 获得一个随机的布尔值(true/false)该函数...【详细内容】
2021-08-19  Tags: JavaScript  点击:(77)  评论:(0)  加入收藏
JavaScript 提供了大量不同的处理数组的方法,这里花几分钟时间介绍 8 个项目中可以用到的数组方法。1. Array.map()使用.map() 方法,可以创建一个基于原始数组的修订版数组。....【详细内容】
2021-08-19  Tags: JavaScript  点击:(95)  评论:(0)  加入收藏
▌简易百科推荐
1、通过条件判断给变量赋值布尔值的正确姿势// badif (a === 'a') { b = true} else { b = false}// goodb = a === 'a'2、在if中判断数组长度不为零...【详细内容】
2021-12-24  Mason程    Tags:JavaScript   点击:(6)  评论:(0)  加入收藏
给新手朋友分享我收藏的前端必备javascript已经写好的封装好的方法函数,直接可用。方法函数总计:41个;以下给大家介绍有35个,需要整体文档的朋友私信我,1、输入一个值,将其返回数...【详细内容】
2021-12-15  未来讲IT    Tags:JavaScript   点击:(20)  评论:(0)  加入收藏
1. 检测一个对象是不是纯对象,检测数据类型// 检测数据类型的方法封装(function () { var getProto = Object.getPrototypeOf; // 获取实列的原型对象。 var class2type =...【详细内容】
2021-12-08  前端明明    Tags:js   点击:(23)  评论:(0)  加入收藏
作者:一川来源:前端万有引力 1 写在前面Javascript中的apply、call、bind方法是前端代码开发中相当重要的概念,并且与this的指向密切相关。本篇文章我们将深入探讨这个关键词的...【详细内容】
2021-12-06  Nodejs开发    Tags:Javascript   点击:(19)  评论:(0)  加入收藏
概述DOM全称Document Object Model,即文档对象模型。是HTML和XML文档的编程接口,DOM将文档(HTML或XML)描绘成一个多节点构成的结构。使用JavaScript可以改变文档的结构、样式和...【详细内容】
2021-11-16  海人为记    Tags:DOM模型   点击:(35)  评论:(0)  加入收藏
入口函数 /*js加载完成事件*/ window.onload=function(){ console.log("页面和资源完全加载完毕"); } /*jQuery的ready函数*/ $(document).ready(function(){ co...【详细内容】
2021-11-12  codercyh的开发日记    Tags:jQuery   点击:(36)  评论:(0)  加入收藏
一、判断是否IE浏览器(支持判断IE11与edge)function IEVersion() {var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串var isIE = userAgent.indexOf("comp...【详细内容】
2021-11-02  V面包V    Tags:Javascript   点击:(40)  评论:(0)  加入收藏
Null、Undefined、空检查普通写法: if (username1 !== null || username1 !== undefined || username1 !== '') { let username = username1; }优化后...【详细内容】
2021-10-28  前端掘金    Tags:JavaScript   点击:(51)  评论:(0)  加入收藏
今天我们将尝试下花 1 分钟的时间简单地了解下什么是 JS 代理对象(proxies)?我们可以这样理解,JS 代理就相当于在对象的外层加了一层拦截,在拦截方法里我们可以自定义一些个性化...【详细内容】
2021-10-18  前端达人    Tags:JS   点击:(51)  评论:(0)  加入收藏
带有多个条件的 if 语句把多个值放在一个数组中,然后调用数组的 includes 方法。// bad if (x === "abc" || x === "def" || x === "ghi" || x === "jkl") { //logic } // be...【详细内容】
2021-09-27  羲和时代    Tags:JS   点击:(58)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条