一段代码,带你理解js执行上下文的工作流程

BitNebulaVoyant
• 阅读 3130

原文链接,欢迎关注我的博客

我相信很多前端初学者一开始都会被执行上下文这个概念弄晕,或者说似懂非懂。对于工作两年的我来说,说来实在惭愧,虽然知道它大概是什么,但总觉得没有一个更为清晰的认识(无法把它的工作过程描述清楚),因此最近特意温习了一遍,写下了这篇文章

执行上下文

要说清它的大体工作流程,需要提前说明三个基本概念,分别是thread of exection(线程)variable envirnoment(变量环境)call Stack(调用栈),这些概念我们或多或少接触过,接下来我会通过一段示例代码,和一系列图片,进一步解释这三个概念在执行上下文的运作流程。

一段代码

const num = 2;

function addOne(input) {
  const output = input + 1;
  return output;
}

const result = addOne(2);

这段代码做了什么

在运行上面这些代码前,js 引擎做的第一件是就是创建一个global execution context,也就是全局执行上下文:

一段代码,带你理解js执行上下文的工作流程
先看图中的黑色箭头,它表示线程thread的执行顺序,众所周知 js 是单线程的,它会一行行、从上往下去执行代码;而右边的global memory,它用于存储当前上下文中的数据,由于线程目前处于全局上下文环境,故加了个global的前缀。

在这段代码中,第一行我们声明了一个名为num的不可变变量,并赋值为4, 因此global memory中就会分配内存,存储这个变量:

一段代码,带你理解js执行上下文的工作流程
接着继续,当线程执行到第二行时,问题就来了:我们创建了一个addOne的变量,并把一个函数赋值于它,那在global memory里,到底存的是个啥?为了解答这个问题,我特意打印了一下:

function addOne(input) {
  const output = input + 1;
  return output;
}
console.log(addOne);

一段代码,带你理解js执行上下文的工作流程
看,我们竟然把函数里的内容完完整整打印出来了,很明显,它存的是一个函数内部的“文本信息”。

其实很容易理解,当执行第二行的时候,该函数并没有被调用,因此线程不会立刻解析里面的内容,而是把它内部的信息以“文本内容”的形式保存下来,当需要执行的时候,才去解析变量里的函数内容,这也很好地解析了为什么函数内的异常仅会在函数被调用时才抛出来。

因此这时global execution context长这样:

一段代码,带你理解js执行上下文的工作流程
由于addOne里保存的是函数内容,目前对于线程而言它是未知的,因此我们这里特意用一个带有输入输出箭头的函数图标,代表它是一个未被解析的函数。

我们继续执行第三步:还是创建了一个变量result,但此时它被赋予undefined,因为线程暂时无法从addOne这个函数里获知它的返回值。

一段代码,带你理解js执行上下文的工作流程
由于addOne函数被调用了,线程会从刚刚保存的addOne变量中取出内容,去解析、执行它。这时 js 就创建了一个新的执行上下文——local execution context,即当前的执行上下文,这是一个船新的上下文,因此我特意用一个新图片去描述它:

一段代码,带你理解js执行上下文的工作流程
首先这个memory我加了个local前缀,表面当前存储的都是此上下文中的变量数据。无论是上述的global memory,亦或是现在、或未来的local memory,我们可以用一个更为专业的术语variable envirnoment去描述这种存储环境。

此外,这个黑箭头我特意画了个拐角,意味它是从全局上下文进来的,local memory首先会分配内存给变量input,它在调用时就被2赋值了,紧接着又创建了一个output标签并把计算结果3赋值给它。最后,当线程遇到离开上下文的标识——return,便离开上下文,并把ouput的结果一并返回出去。

此时,这个上下文就没用了(被执行完了),于是乎垃圾回收便盯上了它,选择一个恰当的时机把里面的local memory删光光。

这时候有同学会问道:你这个只是普通场景,那闭包怎么解释呢?

其实闭包是个比较大的话题,这里也可以简单描述下:return的是一个函数的话,它返回的不仅是函数本身,还会把local memory中被引用的变量作为此函数的附加属性一并返回出去,这就好比印鱼喜欢吸附在鲨鱼身上一般,鳝鱼无论去哪都带着它,因此,无论这个函数在哪里被调用,它都能在它本身附带的local memory中找到那个变量。如果你把返回的函数console.log出来,也是能够找到它的,这里就不详说,关于闭包的更多概念(包括在函数式编程中的使用),有兴趣的童鞋可以看看这篇文章:Partial & Curry - 函数式编程

go on,回到了global execution contextresult不再孤零零地undefined,而是拿到了可爱的3:

一段代码,带你理解js执行上下文的工作流程

到这里,我们的线程就完成了所有工作,可以歇息了,等等.....好像漏了什么没讲.....对,就是Call Stack

刚刚我们全篇在讲解thread of exection多么努力地一行行解析执行代码,variable envirnoment多么勤快地存储变量,那call stack干了什么?是在偷懒吗?

其实并不是,call stack起到了非常关键的作用:有了它,线程随时可以知道自己目前处于哪个上下文。

试想一下,我们在写代码的过程中往往喜欢在各种函数内穿插着各种子函数,比如递归,因此勤劳的线程就得不断地进入上下文、退出上下文、再进入、再进入、再退出、再进入,久而久之线程根本就不知道自己处于哪个上下文中,也不知道应该在哪个memory中取数据,全都乱套了,因此必须通过一种方式去跟踪、记录目前线程所处的环境。

call stack就是一种数据结构,用于“存储”上下文,通过不断推入push、推出pop上下文的方式,跟踪线程目前所处的环境——线程无需刻意记住自己身处何方,只要永远处于最顶层执行上下文,就是当前函数执行的正确位置

程序一开始运行的时候,call stack先会pushglobal execution context:
一段代码,带你理解js执行上下文的工作流程
接着我们在上述代码第三行调用了addOnecall stack立刻将addOne的上下文push进去,待执行到return标识后,再pop出来:

一段代码,带你理解js执行上下文的工作流程
同样的,即使在复杂的情况,只要遵循pushpop以及时刻处于最顶层上下文的原则,线程就可以一直保持在正确的位置上:

一段代码,带你理解js执行上下文的工作流程
值得一提的是,call stack层数是有上限的,因此稍加不注意,你写的递归可能会造成栈溢出了。

总结

简单来说,上下文就是个可以用于执行代码的环境,与它相关的有三个重要的概念:

  • thread of exection(执行线程) 它的主要职责是,从上到下,一行一行地解析和执行代码,不会同时多处执行,也就是我们常挂在嘴边的“js 是单线程的啦”
  • variable envirnoment(变量环境) 活跃的用于存储数据的内存
  • call stack(调用栈) 一种用于存储上下文,跟踪当前线程所属的上下文位置的数据结构
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
BitNebulaVoyant
BitNebulaVoyant
Lv1
我有故人抱剑去,斩尽春风未肯归。
文章
4
粉丝
0
获赞
0