介绍你有没有问过自己“这在幕后是如何工作的?”。我知道我有。在接下来的系列文章中,我们将深入JS的世界,从引擎到它在幕后是如何工作的,从引擎到提升、执行上下文、词法环境等概念。概念可以让我们更好地理解代码,在我们的工作中有更好的表现,而且,它在面试中非常有用。这可能是一个非常有趣的主题……在我们开始之前,还有一件重要的事情要提一下——每个JS引擎的构建方式都不同,因此不可能涵盖它们的工作原理。所以我们将探讨V8是如何工作的,但其他引擎中的概念仍然非常相似,只是其中一些引擎的实现方式可能有所不同。以下是V8工作原理的概述:如果您还不了解这些内容,请不要担心,您将在本文末尾了解图中的每个步骤。所以,顺便说一句!环境计算机、编译器甚至浏览器实际上都无法“理解”用JS编写的代码。如果是这样,代码如何运行?在幕后,JS总是在特定的环境中运行,最常见的是:浏览器(迄今为止最常见的)Node.js(这是一个运行时环境,允许您通常在服务器中)运行JS)引擎所以JS需要运行在特定的环境中,但是那个环境到底是什么?当你在JS中编写代码时,你会以人类可读的语法编写它,包括字母和数字。如前所述,此类代码无法被机器理解。这就是每个环境都有引擎的原因。通常,引擎的工作是获取该代码并将其转换为以机器代码编写的代码,最终可以由计算机的处理器运行。每个环境都有自己的引擎,最常见的是ChromeV8(Node也使用)、FirefoxSpiderMonkey、Safari的JavaScriptCore和IE的Chakra。所有引擎的工作原理相似,但每个引擎之间存在差异。同样重要的是要记住,引擎在幕后只是一个软件,例如ChromeV8是用C++编写的软件。解析器所以我们有一个环境,在那个环境中有一个引擎。引擎在执行代码时做的第一件事就是用解析器检查它。解析器理解JS语法和规则,它的工作是逐行检查代码并检查代码在语法上是否正确。如果解析器遇到错误,它会停止并发出错误。如果代码有效,解析器会生成称为“抽象语法树”(简称AST)的东西抽象语法树(AST)因此,我们的环境中有一个引擎,其中包含一个生成AST的解析器。但什么是AST,我们为什么需要它?AST是一种数据结构,它不是JS独有的,但实际上被许多其他语言的编译器使用(其中一些语言是Java、C#、Ruby、Python)。AST只是代码的树形表示,引擎创建AST而不是直接编译为机器代码的主要原因是,当您将代码包含在树数据结构中时,转换为机器代码更容易。您实际上可以看到AST的样子,只需将任何代码放入ASTExplorer网站,然后查看创建的数据结构:解释器解释器的工作是获取创建的AST,并将其转换为代码(IR)。一旦我们需要进一步的上下文来完全理解它们的含义,我们就会更多地了解解释器。IntermediateRepresentation(IR)那么,解释器从AST生成的IR是什么?IR是表示源代码的数据结构或代码。它充当用JS等抽象语言编写的代码和机器代码之间的中间步骤。本质上,您可以将IR视为机器代码的抽象。IR有很多种,JS引擎中比较流行的一种是字节码。下图显示了IR在V8引擎中的作用:但是您可能会问……为什么我们需要IR?为什么不直接编译成机器码呢?引擎使用IR作为高级代码和机器代码之间的链接中间步骤有两个主要原因:为Intel处理器编写的机器代码和为ARM处理器编写的机器代码将不同。另一方面,IR可以将两者作为通用匹配,并且可以匹配任何平台。这使得下面的转换过程更加容易和移动。优化-从代码优化和硬件优化的角度来看,使用IR比IR更容易优化。有趣的事实:JS引擎并不是唯一使用字节码作为IR的引擎,在也使用字节码的语言中,你会发现C#、Ruby、Java等。编译器编译器的工作是将IR(字节码在我们的示例)由解释器创建,并通过一些优化将其转换为机器代码。让我们谈谈代码编译和一些基本概念。请记住,这是一个庞大的主题,需要花费大量时间才能掌握,因此我只会针对我们的用例进行一般性介绍。解释器与编译器有两种方法可以使用编译器和解释器将代码转换为机器可以运行的机器语言。解释器和编译器之间的区别在于,解释器翻译您的代码并逐行执行,而编译器在执行前立即将您的所有代码翻译成机器代码。每种方法各有利弊,编译器启动快,但复杂启动慢,解释器简单但启动慢。话虽如此,有3种方法可以将高级代码转换为机器代码并运行它:解释-使用这种策略,您将拥有一个解释器,它逐行执行代码并执行(效率不高)。提前编译(AOT)——在这里你需要一个编译器来首先编译整个代码然后才执行它。Just-in-TimeCompilation——JOT编译策略结合了AOT策略和Interpretation策略,试图两全其美,执行动态编译,并且还允许进行某些优化,这实际上加快了编译过程。我们将详细解释JIT编译。大多数JS引擎使用JIT编译器,但不是全部。例如,ReactNative使用的引擎Hermes并没有使用JIT编译器。总而言之,编译器获取解释器创建的IR,并从中生成优化的机器代码。JITCompiler正如我们所说,大多数JS引擎都使用JIT编译方式。JIT结合了AOT策略和解释,允许进行某些优化。让我们更深入地了解这些优化以及编译器的作用。JIT编译优化是通过重复执行代码并对其进行优化来完成的。优化过程如下:本质上,JIT编译器通过收集执行代码的分析数据来获得反馈,如果它遇到任何热代码段(重复自身的代码),该热段将通过编译器,然后使用此信息以更优化地重新编译。假设您有一个返回对象属性的函数:functionload(obj){returnobj.x;}看起来很简单?也许对我们来说,但对编译器来说就没那么容易了。如果编译器看到一个对象,它什么都不知道,它必须检查属性x的位置,如果对象确实有这样的属性,它在内存中的位置,它在原型链中的位置等等。所以如何优化?要理解这一点,我们必须知道在机器代码中,对象及其类型是被保留的。假设我们有一个具有x和y属性的对象,x是数字类型,y是字符串类型。理论上,这个对象在机器码中会表示如下:如果我们调用具有相同对象结构的函数,就可以进行优化。这意味着属性将相同且顺序相同,但值可以不同,像这样:load({x:1,y:'hello'});load({x:5,y:'world'});load({x:3,y:'foo'});load({x:9,y:'bar'});工作方式如下。调用该函数后,优化编译器会识别出我们正在尝试调用一个已经再次调用过的函数。然后它继续检查作为参数传递的对象是否具有相同的属性。如果是这样,它就已经能够访问其在内存中的位置,而无需遍历原型链和对未知对象执行许多其他操作。本质上,编译器贯穿了一个优化和去优化的过程。当我们运行代码时,编译器假定该函数将使用与之前相同的类型,因此它会使用该类型预先保存代码。这种类型的代码称为优化机器代码。每次代码再次调用相同的函数时,优化编译器都会尝试访问内存中的相同位置。但是由于JS是一种动态类型的语言,在某些时候,我们可能希望对不同的类型使用相同的函数。在这种情况下,编译器将执行反优化过程并正常编译代码。总结一下关于JIT编译器的部分,JIT编译器的工作是通过使用热代码段来提高性能,当编译器执行之前执行过的代码时,它假设类型相同并使用已经优化过的代码生成,但如果类型不同,JIT将执行反优化并正常编译代码。关于性能的说明提高应用程序性能的一种方法是对不同的对象使用相同的类型。如果你有两个相同类型的不同对象,即使值不同,只要属性顺序相同且类型相同,编译器就会将这两个对象视为具有相同的结构和类型并且可以更快地访问它。例如:constobj={x:1,a:true,b:'hey'}constobj2={x:7,a:false,b:'hello'}从示例中可以看出,我们有两个,但是因为属性的顺序和类型相同,编译器将能够更快地编译这些对象。但是即使有可能以这种方式优化代码,我认为还有很多重要的事情要做,以提高性能,但这些事情并不重要。在一个团队中做这样的事情也很困难,而且由于引擎非常快,所以总体上并没有太大的不同。话虽如此,我已经看到V8团队成员推荐了这个技巧,所以也许你有时确实想尝试遵循它。在可能的情况下遵循它不会对我造成任何伤害,但绝对不会以干净的代码和架构决策为代价。ProfileJS代码必须在一个环境中运行,最常见的是浏览器和Node.js。环境需要有一个引擎,可以将以人类可读语法编写的JS代码转换为机器代码。该引擎使用解析器逐行检查代码并检查语法是否正确。如果有任何错误,代码将停止执??行并抛出错误。如果所有检查都通过,解析器将创建一个称为抽象语法树(AST)的树状数据结构。AST是一种以树形结构表示代码的数据结构。通过AST将代码转换为机器代码更容易。然后解释器继续获取AST并将其转换为IR,IR是机器代码的抽象,是JS代码和机器代码之间的中介。IR还可以执行优化,并且更具移动性。然后,JIT编译器通过编译代码、获取动态反馈并使用该反馈改进编译过程,将生成的IR转换为机器代码。原文链接:https://medium.com/coralogix-engineering/how-js-works-behind-the-scenes-the-engine-9f15bba95a15)
