【转】JavaScript 执行上下文——JS 的幕后工作原理

这篇具有很好参考价值的文章主要介绍了【转】JavaScript 执行上下文——JS 的幕后工作原理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

转自译文:JavaScript 执行上下文——JS 的幕后工作原理 。译文中图片不显示,要结合原文看,看着不方便。整理了一份含图片的。所以有了此篇的转载,以方便阅读。

以下是正文:

原文:JavaScript Execution Context – How JS Works Behind The Scenes,作者:Victor Ikechukwu

所有JavaScript代码都需要在某种环境中托管运行。在大多数情况下,网络浏览器就是这个环境。

当一段JavaScript代码在网络浏览器中执行时,幕后发生很多事情。在这篇文章中,我们将对运行在浏览器的JavaScript代码的幕后一探究竟。

在深入研究前,需要先了解一些概念,因为这些概念会在后文反复提及。

  • 编译器:编译器或语法编译器是一个能够逐行读取代码的程序。它了解代码如何匹配编程语言所定义的语法,以及代码应该做什么。

  • JavaScript引擎:JavaScript引擎是一个计算机程序,它接收JavaScript源代码并将其编译成CPU可以理解的二进制指令(机器码)。JavaScript引擎通常是由浏览器供应商开发的,每一个主流浏览器都有一个自己开发的引擎。如:谷歌Chrome浏览器的 V8引擎,Firefox的SpiderMonkey和IE的Chakra。

  • 函数声明:指的是被命名的函数。

    function doSomething() { //"doSomething" 为函数名
    	statements; 
    } 
    
  • 函数表达式:指的是匿名函数,即没有函数名称的函数,如: js function () { statements }。 通常在表达式中使用,如把变量赋值为一个函数。let someValue = function () { statements }

    概念解释完毕,让我们开始深入研究吧!

JavaScript是如何被执行的

你或许不知道,浏览器并不理解我们在应用中编写的高级JavaScript代码。代码需要被转换成浏览器和计算机能够理解的格式——机器码。

浏览器在读取HTML时,如果遇到了<script> 标签或包含JavaScript代码的属性如onClick,会发送给JavaScript引擎。

浏览器的JavaScript引擎会创造一个特殊的环境来处理这些JavaScript代码的转换和执行。这个特殊的环境被称为执行上下文

执行上下文包含当前正在运行的代码和有助于其执行的所有内容。

在执行上下文运行期间,编译器解析代码,内存存储变量和函数,可执行的字节码生成后,代码执行。

JavaScript中有两种执行上下文:

  • 全局执行上下文(GEC)
  • 函数执行上下文(FEC)

让我分别一探究竟。

全局执行上下文(GEC)

每当 JavaScript 引擎接收到脚本文件时,它首先会创建一个默认的执行上下文,称为 全局执行上下文 (GEC)

GEC是基础/默认的执行上下文,所有 不在函数内部的JavaScript代码都在这里执行。

每一个JavaScript文件只能有一个GEC。

函数执行上下文(FEC)

每当函数被调用时,JavaScript引擎就会在GEC内部创建另一种执行上下文,称为函数执行上下文(FEC),并在FEC中评估和执行函数中的代码。

因为每个函数调用都创建自己的FEC,所以在脚本运行期间会有多个FEC。

执行上下文是如何被创建的?

我们已经了解了什么是执行上下文以及不同种类的执行上下文,现在让我们来看看执行上下文是如何被创建的。

执行上下文(GEC或FEC)的创建分为两个阶段:

  1. 创建阶段
  2. 执行阶段

创建阶段

在创建阶段,执行上下文首先与执行上下文对象(ECO)相关联。执行上下文对象存储了许多重要的数据,执行上下文中的代码在运行时会使用这些数据。

创建阶段分三个步骤来定义和设置执行上下文对象的属性:

  1. 创建变量对象(VO)
  2. 创建作用域链
  3. 设置 this关键字的值

我们来具体聊聊每一个步骤。

创建阶段:创建变量对象(VO)

变量对象(VO)是一个在执行上下文中创建的类似于对象的容器,存储执行上下文中变量和函数声明。

在GEC中,每当使用var关键字声明变量,VO就会添加一个指向该变量的属性,并将值设置为"undefined"。

同时,每当函数声明时,VO就会添加一个指向该函数的属性,并将这个属性存储在内存中。这就意味着在开始运行代码之前,所有函数声明就已经存储在VO中,并可以在VO中访问。

但在FEC中并不创建VO,而是生成一个类数组对象,称为arguments对象,包含传入函数的所有参数。想要进一步了解arguments对象,可以参见此处。

这种将变量和函数声明存储在内存中优先于执行代码的过程被称为提升。因为这个概念特别重要,所以我先简单介绍一下这个概念。

JavaScript中的提升

函数和变量在JavaScript中被提升,指的是它们被存储在当前执行上下文VO的内存中,并且在代码开始执行之前就可以在执行上下文中访问。

函数提升

在构建应用的大多数情况下,开发者选择在脚本顶部定义函数,之后再调用,如下图:

【转】JavaScript 执行上下文——JS 的幕后工作原理

归功于提升,反过来操作也奏效。可以先调用函数,再定义这个被调用的函数。

【转】JavaScript 执行上下文——JS 的幕后工作原理

在上述代码中, getAge 函数将被存储在VO的内存中,所以可以在定义前被访问。

变量提升

var关键字声明的变量会作为属性存储在当前执行上下文VO中,值被设置为undefined。也就是说和函数不同,想要在变量声明访问变量值会得到undefined

【转】JavaScript 执行上下文——JS 的幕后工作原理

提升的基本原则

提升只对函数声明有效,函数表达式并不能被提升。下面是一个函数表达式的例子,代码执行会中断:

getAge(1990); 
var getAge = function (yearOfBirth) {
	console.log(new Date().getFullYear - yearOfBirth) 
};

代码执行被中断的原因是,如果使用函数表达式,getAge会被当作变量而非函数。由于变量提升,变量的值被设置为undefined。所以我们会得到以下报错:

【转】JavaScript 执行上下文——JS 的幕后工作原理

同样的,变量提升也不适用于由let或者const关键字声明的变量。尝试在声明前访问letconst关键字声明的变量,会得到ReferenceError报错。

这种情况下变量会被提升,但是没有被设置为默认值 undefinedjs console.log(name); let name = "Victor"; 会报错:

【转】JavaScript 执行上下文——JS 的幕后工作原理

创建阶段:创建作用域链

创建完变量对象(VO),紧接着就是执行上下文的创建阶段的下一步——创建作用域链。

JavaScript中的作用域链是一个机制,决定了一段代码对于代码库中其他一些代码来说的可访问性。作用域回答这样一些问题:一段代码可以在哪里访问?哪里不能访问?代码哪些部分可以被访问,哪些部分不能?

每一个函数执行上下文都会创建一个作用域:作用域相当于是一个空间/环境,变量和函数定义在这个空间里,并且可以通过一个叫做作用域查找的过程访问。

也就是说代码被写入代码库的位置,就是这段代码被读取的位置。

如果函数被定义在另一个函数内部,处在内部的函数可以访问自己内部的代码以及外部函数(父函数)的代码。这种行为被称作词法作用域查找

但外部函数并不能访问内部函数的代码。

作用域的概念就引出了JavaScript另一个相关的现象——闭包。闭包指的是内部函数永远可以访问外部函数中的代码,即便外部函数已经执行完毕。想要了解更多闭包相关的信息,你可以点击这里。

我们来看几个例子加深理解:

【转】JavaScript 执行上下文——JS 的幕后工作原理
)

  • 右边是全局作用域,一旦.js文件加载就会创建这个默认作用域,整个代码中所有函数都可以访问。
  • 红色方框里的是first函数的作用域,在这里定义了变量b='Hello!'second函数。

【转】JavaScript 执行上下文——JS 的幕后工作原理

  • 绿色方框里的是second函数的作用域,这里有一个console.log语句,用于打印a,bc

除了变量c,变量ab并不是在second函数中定义的。但因为词法作用域查找,second函数可以访问父作用域中的变量。

在执行这段代码的时候,JS引擎在second函数作用域中找不到变量b,所以它会向上查找其父作用域,从first函数开始,在这里它找到变量b ='Hello!',于是就回到second函数并解析变量b

变量a的处理也是一样。JS引擎一直向上查找父作用域直至GEC作用域,并在second函数中解析a的值。

JavaScript引擎一路向上遍历执行上下文直至解析处在函数内部触发的变量和函数的概念就叫作用域链

【转】JavaScript 执行上下文——JS 的幕后工作原理

仅当JS引擎无法在作用域链中解析变量,才会停止并报错。

但反向查找并不奏效,也就是说全局作用域永远无法访问函数内部的变量,除非这些变量被函数返回

作用域链就好像一个单向玻璃,你可以从内部看到外面,但是外面的人却看不见你。

这也就是为什么在图片中大红色的箭头是指向上方的,作用域链是单向度的。

创建阶段:设置this关键字的值

作用域查找之后就是创建阶段的最后一步是设置this关键字的值。

JavaScript中this关键字指的是执行上下文所属的作用域。

一旦作用域链被创建,JS引擎就会初始化this关键字的值。

全局上下文中的"this"

在GEC(所有函数和对象之外)中,this指向全局对象——window对象。

同时,由var关键字初始化的函数声明和变量会被作为全局对象(window对象)的方法或者属性。

这就意味着,在任何函数外声明的变量和函数,如下:

var occupation = "Frontend Developer"; 

function addOne(x) { 
    console.log(x + 1) 
}

都可以写作:

window.occupation = "Frontend Developer"; 
window.addOne = (x) => { 
	console.log(x + 1)
};

在GEC中的函数和变量会被当作window对象的方法和属性,所以下面的代码片段的返回值为真:

【转】JavaScript 执行上下文——JS 的幕后工作原理

函数中的"this"

在FEC中,并没有创建this对象,而是能够访问this被定义的环境。

下面的例子中,定义环境为window对象,因为函数被定义在GEC中:

var msg = "I will rule the world!"; 

function printMsg() { 
    console.log(this.msg); 
} 

printMsg(); // logs "I will rule the world!" to the console.

在对象中,this关键字并不指向GEC,而是指向对象本身。引用对象中的this如同引用:

对象.定义在对象内部的属性或方法;

考虑下面的代码:

var msg = "I will rule the world!"; 
const Victor = {
    msg: "Victor will rule the world!", 
    printMsg() { console.log(this.msg) }, 
}; 

Victor.printMsg(); // logs "Victor will rule the world!" to the console.

控制台打印出"Victor will rule the world!"而非"I will rule the world!"。因为在这个例子中,函数可以访问的this关键字的值是定义其的对象,而不是全局对象。

this关键字的值设置后,执行上下文对象的所有属性就定义完成,创建阶段结束,JS引擎就进入到执行阶段。

执行阶段

执行上下文创建阶段之后就是执行阶段了,在这一阶段代码执行真正开始。

到目前为止,VO包含的变量值为undefined,如果这时就运行代码,肯定会报错,我们无法执行未定义的变量。

在执行阶段,JavaScript引擎会再次读取执行上下文,并用变量的实际值更新VO。编译器再把代码编译为计算机可执行的字节码后执行。

JavaScript执行栈

执行栈又称调用栈,记录了脚本整个生命周期中生成的执行上下文。

JavaScript是单线程语言,也就是说它只能在同一时间执行一项任务。因此,其他的操作、函数和事件发生时,执行上下文也会被创建。由于单线程的特性,一个堆叠了执行上下文的栈就会被创建,称为执行栈

当浏览器加载脚本,JS引擎从全局上下文也就是默认上下文开始执行代码,所以全局上下文被放在执行栈的最底部。

然后JS引擎再搜索代码中被调用的函数。每一次函数被调用,一个新的FEC就会被创建,并被放置在当前执行上下文的上方。

执行栈最顶部的执行上下文会成为活跃执行上下文,并且始终是JS引擎优先执行。

一旦活跃执行上下文中的代码被执行完毕,JS引擎就会从执行栈中弹出这个执行上下文,紧接着执行下一个执行上下文,以此类推。

为了了解执行栈的工作流,请考虑下面的代码:

var name = "Victor";

function first() {
  var a = "Hi!";
  second();
  console.log(`${a} ${name}`);
}

function second() {
  var b = "Hey!";
  third();
  console.log(`${b} ${name}`);
}

function third() {
  var c = "Hello!";
  console.log(`${c} ${name}`);
}

first();

首先,JS引擎加载脚本。

然后,JS引擎创建GEC,并把其放置在执行栈的最底部。

【转】JavaScript 执行上下文——JS 的幕后工作原理

name变量在所有函数外部定义,所以位于GEC,并且被VO存储。

同样的步骤也发生在firstsecondthird函数。

别被GEC中的函数迷惑了。记住,GEC只适用于不在任何函数内部的JavaScript代码(变量和函数)。因为它们没有被定义在任何函数内部,而是定义在GEC中。是不是清晰很多😃?

当JS引擎遇到first函数调用时,一个新的FEC被创建。新的执行上下文被放置在当前上下文上方,形成执行栈

【转】JavaScript 执行上下文——JS 的幕后工作原理

first函数调用时,其执行上下文变成活跃执行上下文,JavaScript首先执行。

first函数中的变量a ='Hi!'被存储在其FEC中,而非GEC中。

接着,second函数在first函数中被调用。

由于JavaScript单线程的特性,first函数的执行会被暂停,直到second函数执行完闭,才会继续执行。

同样的,JS引擎会给second函数设置一个新的FEC,并把它放置在栈顶端,并激活。

【转】JavaScript 执行上下文——JS 的幕后工作原理

second函数成为活跃执行上下文,变量b = 'Hey!'被存储在其FEC中,之后在second函数中的third函数被调用,其FEC被创建并放置在执行栈的顶部。

【转】JavaScript 执行上下文——JS 的幕后工作原理

third函数中的变量c = 'Hello!'被存储在其FEC中,Hello! Victor在控制台中打印。

因为函数执行了所有任务,并且返回, 其FEC就从栈顶端弹出,而调用third函数的second函数重新成为活跃执行上下文。

【转】JavaScript 执行上下文——JS 的幕后工作原理

回到second函数,控制台打印Hey! Victor。函数完成所有任务,返回,这个执行上下文从执行栈上弹出。

【转】JavaScript 执行上下文——JS 的幕后工作原理

first函数执行完毕,从执行栈上弹出后,控制流回到代码的GEC。

【转】JavaScript 执行上下文——JS 的幕后工作原理

总结

JavaScript执行上下文是正确理解其他基础概念的核心。

代码得以运行归功于Js引擎处理执行上下文(GEC和FEC)以及调用栈。

希望通过这篇文章,你已经更了解你编写的函数和代码是按照什么顺序运行的,以及JS引擎是如何处理这些代码的。

理解下面这些概念,会帮助你成为一个更好的开发者:

  • 熟悉一门语言的输入和输出。
  • 大致理解一门语言的内在/核心概念。
  • 如何编写简洁、易维护和结构清晰的代码,降低bug的风险。

希望这篇文章对你有帮助。欢迎转发这篇文章。欢迎在 Twitter上关注我和我互动。 我的博客 上也有一些免费的教学文章和资源。你的反馈将激励我贡献更多!

感谢阅读,编码快乐!

------------------------------- 以上就是原文,下面相关资料是我自己加的哈~ -----------------------------------

相关的资料

图解Javascript——执行上下文

图解 变量对象 --> 活动对象

图解 avascript——作用域、作用域链、闭包文章来源地址https://www.toymoban.com/news/detail-807270.html

到了这里,关于【转】JavaScript 执行上下文——JS 的幕后工作原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 执行上下文

    通过var定义(声明)的变量--在定义语句之前就可以访问到 值为undefined 通过function声明的函数--在之前就可以直接调用 值为函数定义(对象) 全局代码 函数(局部)代码 在执行全局代码前将window确定为全局执行上下文 对全局数据进行预处理 var定义的全局变量--undefined--添加

    2023年04月20日
    浏览(57)
  • 从零开始理解Linux中断架构(7)--- Linux执行上下文之中断上下文

            当前运行的loop是一条执行流,中断程序运行开启了另外一条执行流,从上一节得知这是三种跳转的第三类,这个是一个大跳转。对中断程序的基本要求就是 中断执行完毕后要恢复到原来执行的程序 ,除了时间流逝外,原来运行的程序应该毫无感知。        

    2024年02月11日
    浏览(67)
  • docker的上下文和工作目录

    在编写Dockerfile时我们总会接触到COPY上下文和工作目录,有时候 这些位置 搞不清楚,总是让我们陷入困境,本文就一起来看下 这2个路径 。 Dockerfile文件的COPY指令,拷贝的源文件就是基于上下文目录来查找的,到底什么是上下文路径,我们需要先来看下,而要解释清楚什么是

    2023年04月20日
    浏览(36)
  • 从 ECMAScript 6 角度谈谈执行上下文

    大家好,我是归思君 起因是最近了解JS执行上下文的时候,发现很多书籍和资料,包括《JavaScript高级程序设计》、《JavaScript权威指南》和网上的一些博客专栏,都是从 ES3 角度来谈执行上下文,用ES6规范解读的比较少,所以想从ES6的角度看一下执行上下文。 下面我尝试用E

    2024年02月04日
    浏览(40)
  • 〖大前端 - 基础入门三大核心之JS篇(51)〗- 面向对象之认识上下文与上下文规则

    说明:该文属于 大前端全栈架构白宝书专栏, 目前阶段免费 , 如需要项目实战或者是体系化资源,文末名片加V! 作者:哈哥撩编程,十余年工作经验, 从事过全栈研发、产品经理等工作,目前在公司担任研发部门CTO。 荣誉: 2022年度博客之星Top4、2023年度超级个体得主、谷

    2024年02月05日
    浏览(61)
  • CPU上下文切换原理剖析

    CPU上下文其实是一些环境正是有这些环境的支撑,任务得以运行,而这些环境的硬件条件便是CPU寄存器和程序计数器。CPU寄存器是CPU内置的容量非常小但是速度极快的存储设备,程序计数器则是CPU在运行任何任务时必要的,里面记录了当前运行任务的行数等信息,这就是CPU上

    2024年02月11日
    浏览(42)
  • 76 # koa 上下文的实现原理

    上一节实现了 koa 基本逻辑实现以及属性的扩展,下面继续实现上下文的实现 ctx 跟 proto 的关系 MDN: defineGetter 备注: 此特性已弃用,建议使用对象初始化语法或 Object.defineProperty() API 来定义 getter。该方法的行为只针对 Web 兼容性进行了规定,在任何平台上都不需要实现该方

    2024年02月10日
    浏览(37)
  • JavaScript-----禁止鼠标选中文字和禁止鼠标右键上下文菜单

     selectstart   开始选中 还有一些别的方法,可以参考一下: 禁止鼠标右键 οncοntextmenu=\\\"return false\\\" 禁止选择 onselectstart=\\\"return false\\\" 禁止拖放 οndragstart=\\\"return false\\\" 禁止拷贝 οncοpy=document.selection.empty() 禁止复制 oncopy = \\\"return false\\\" 禁止粘贴 input type=text οnpaste=\\\"return false\\\" 禁止剪贴

    2024年02月10日
    浏览(53)
  • JS引擎中的线程,事件循环,上下文

      线程 浏览器中有哪些进程呢? 1.浏览器进程:浏览器的主进程,负责浏览器的界面界面显示,与用户交互,网址栏输入、前进、后退,以及页面的创建和销毁。 2.渲染进程(浏览器内核):默认一个tab页面一个渲染进程,主要的作用为页面渲染,脚本执行,事件处理等。 3.

    2024年02月08日
    浏览(91)
  • js中this关键字的作用和如何改变其上下文

    一、this 的作用 JavaScript 中的 this 引用了所在函数正在被调用时的对象。在不同的上下文中, this 的指向会发生变化。 在全局上下文中, this 指向全局对象(在浏览器中是 window 对象,在 Node.js 中是 global 对象)。 在函数中, this 指向调用该函数的对象。如果该

    2024年02月07日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包