初学者带你学习JavaScript引擎如何工作
一些名词
JS引擎——一个读取代码并运行的引擎,没有单一的“JS引擎”;每个浏览器都有自己的引擎,比如谷歌就有v。
范围—可以访问变量的“区域”。
词法范围——词法阶段的范围,换句话说,词法范围是由你写代码时写变量的位置和块范围决定的,所以词法分析器处理代码时范围会保持不变。
块范围-由大括号{}创建的范围
作用域链——一个函数可以上升到它的外部环境(词汇上)去搜索一个变量,它可以向上搜索直到它到达全局作用域。
同步——一次一件事,同步引擎一次只执行一行,JavaScript是同步的。
异步——同时做多件事,JS通过浏览器API模拟异步行为
event Loop)-浏览器API完成调用函数的过程,将回调函数推送到回调队列,然后在堆栈为空时将回调函数推送到调用堆栈。
堆栈—只能将元素推入并弹出顶部元素的数据结构。想想堆一个之字形的塔;你不能删除中间的块,后进先出。
堆—变量存储在内存中。
调用堆栈-函数调用的队列,它实现堆栈数据类型,这意味着一次可以运行一个函数。调用函数将它推到堆栈上,并从函数中返回它以将其弹出堆栈。
执行上下文——当一个函数被放在调用栈上时,由JS创建的环境。
闭包——当一个函数在另一个函数中创建时,它会“记住”它后来被调用时创建的环境。
垃圾收集—当内存中的变量被自动删除时,引擎将处置它,因为它不再被使用。
变量的提升-当变量内存没有被分配时,它将被提升到全局的顶部并被设置为undefined。
这是一个变量/关键字,由JavaScript为每个新的执行上下文自动创建。
调用栈(调用栈)
请看下面的代码:
var myOtherVar=10 function a(){ console . log(' myVar ',myVar)b()} function b(){ console . log(' mythervar ',mythervar)c()} function c(){ console . log(' Hello world!')}a()var myVar=5有几点需要注意:
变量声明的位置(上面一个下面一个)函数A调用下面定义的函数B,函数B调用函数C执行时你预计会发生什么?是因为b在a后面声明,还是一切正常,所以有错误?console.log打印的变量呢?
以下是打印结果:
myVar ' undefined ' myothervar ' 10 ' Hello world!分解以上步骤。
1.变量和函数声明(创建阶段)
第一步是为内存中的所有变量和函数分配空间。但是,请注意,除了undefined之外,没有为变量赋值。因此,打印时myVar的值是未定义的,因为JS引擎从顶部开始逐行执行代码。
与变量不同,函数可以一次声明和初始化,这意味着它们可以在任何地方被调用。
所以上面的代码如下所示:
varmyotherv=undefined var myvar=undefined function a(){ 0.}函数b(){ 0.}函数c(){ 0.}这些都存在于JS创建的全局上下文中,因为它位于全局空间中。
在全球范围内,JS还补充道:
全局对象(浏览器中的窗口对象和NodeJs中的全局对象)这指向全局对象2。执行
接下来,JS引擎逐行执行代码。
MyOtherVar=10在全局上下文中,MyOtherVar被赋值为10
所有函数都已创建,下一步是执行函数a()
每次调用函数时,都会为函数创建一个新的上下文(重复步骤1)并放入调用堆栈。
函数a () {console.log ('myvar ',myvar) b ()}如下:
创建新的函数上下文。函数不声明变量和函数。这是在函数内部创建的,指向一个全局对象(窗口),然后引用外部变量myVar。myVar属于全局范围。然后调用函数b,函数b的过程和a一样,这里不做分析。以下调用堆栈执行图:
创建全局上下文、全局变量和函数。每个函数调用都会创建一个上下文、一个对外部环境的引用以及这个。函数执行后,从栈中弹出,其执行上下文由垃圾收集收集(闭包除外)。当调用堆栈为空时,它将从事件队列中获取事件。范围和范围链
在前面的例子中,一切都是全局范围的,这意味着我们可以从代码中的任何地方访问它。现在,介绍私有范围以及如何定义范围。
功能/词汇范围
考虑以下代码:
函数a() {var myOtherVar='在A'b()内部}函数b() {var myVar='在B'console.log('myOtherVar: ',myOtherVar)函数c () {console.log ('myvar: ',myVar)} c()} var mythervar=' global other var ' var myVar=' global myVar ' A()应注意以下几点:
变量函数c在全局范围和函数内部声明,打印结果现在在函数b中声明如下:
Myothervar: '全局othervar' myvar3360 '在b '内执行步骤:
全局创建和声明——在内存中创建所有函数和变量,并执行全局对象和这个——它逐行读取代码,为变量赋值,并执行函数A,后者创建一个新的上下文并将其放在堆栈上,然后调用函数B,后者也创建一个新的上下文,并将其放在堆栈上。函数B在函数B的上下文中创建myVar变量,并声明函数C将为上面提到的每个新上下文创建外部引用。外部引用取决于堆栈中的函数。
函数b试图打印myOtherVar,但是这个变量在函数b中不存在,所以函数b将使用它的外部引用来查找范围链。因为函数b是全局声明的,而不是在函数a内部声明的,所以它使用全局变量myOtherVar。函数c执行相同的步骤。由于函数c本身没有变量myVar,所以通过作用域链也就是函数b来查找,因为myVar是在函数b内部声明的,下面是实现图:
请记住,外部引用是单向的,不是双向的。例如,函数b不能直接跳到函数c的上下文中,并从那里获取变量。
最好把它想象成一个只能单向运行的链条(范围链)。
A-global c-b-global在上图中,您可能会注意到函数是创建新范围的一种方式。(全局范围除外)但是,还有另一种创建新范围的方法,即块范围。
块范围
在下面的代码中,我们有两个变量和两个循环。如果在循环中重新声明相同的变量,会打印出什么(反正我弄错了)?
函数LoopScope(){ var I=50 var j=99 for(var I=0;i 10i ) {}console.log('i=',i)for(让j=0;歼10;J) {} console.log ('j=',j)} loopscope()打印结果:
I=10j=99第一个循环覆盖了var i,这可能会导致不知情的开发人员出现bug。
在第二个循环中,每次迭代都创建自己的范围和变量。这是因为它使用了let关键字,该关键字与var相同,只是let有自己的块范围。另一个关键字是const,和let一样,但是const是常量,不能更改(指内存地址)。
块范围是由大括号{}创建的范围
再看一个例子:
函数block scope(){ let a=5 { const blocked var=' blocked ' var b=11a=9000 } console . log(' a=',a) console.log ('b=',b) console.log ('blocked var=',blocked var)}。
A=9000b=11引用错误:未定义blocked var a是一个块范围,但它在函数中,而不是嵌套的。在这个例子中,var是相同的。对于块范围内的变量,其行为类似于函数。请注意,var b可以从外部访问,但const blockedVar不能。在块中,从范围链中找到一个,并将字母a更改为9000。使用块范围可以使代码更加清晰和安全,应该尽可能多地使用它。
事件循环(事件循环)
接下来,看看事件循环。这是回调、事件和浏览器API工作的地方
我们讨论不多的是堆,也叫全局内存。它是存储变量的地方。因为了解JS引擎如何实现其数据存储的实际用途并不多,这里就不讨论了。
到异步代码:
函数log message 2(){ console . log(' message 2 ')} console . log(' message 1 ')settimeout(log message 2,1000) console.log ('message3 ')上面的代码主要是打印一些消息到控制台。使用setTimeout函数来延迟消息。我们知道js是同步的。让我们看看输出
消息1消息3消息2打印消息1呼叫设置输出打印消息3打印消息2记录消息3
稍后,它记录消息2
SetTimeout是一个API。像大多数浏览器API一样,当它被调用时,它会向浏览器发送一些数据和回调。在我们这边,我们将消息2的打印延迟了一秒钟。
在调用setTimeout之后,我们的代码继续不间断地运行,打印Message 3并执行一些必须首先执行的操作。当浏览器等待一秒钟时,它会将数据传递给我们的回调函数,并将其添加到事件/回调队列中。然后呆在
在队列中,只有当* *调用堆栈* *为空时,才会将其推入堆栈。
代码示例
熟悉JS引擎最好的方法就是使用它,并给出一些有意义的例子。
简单闭合
在这个例子中,有一个函数返回一个函数,并且在返回的函数中使用了外部变量,这被称为闭包。
函数指数(x){返回函数(y){//与math.pow()相同或x为y次方,返回y * * x}} const square=指数(2) console.log (square (2),square (3))//4,9 console.log(指数
如果我们使用无限循环,调用堆栈将被填满。发生什么情况,回调队列将被阻塞,因为回调队列只能在调用堆栈为空时添加。
函数阻塞代码(){const starttime=newdate()。getseconds()//将函数settimeout(function(){在=newdate()调用的const)延迟250毫秒。getseconds()Const diff=called at-startTime//打印调用此函数console.log所需的时间(`在: $ {diff}秒后调用的回调')}。250)//用循环阻塞堆栈2秒,同时(true) {const currenttime=newdate()。getseconds()//exit if(current time-start time=2)break } }阻塞代码()//Callback在:秒后调用'我们尝试在250毫秒后调用一个函数,但由于我们的循环阻塞堆栈需要两秒钟,回调函数实际上会在两秒钟后执行,这是JavaScript应用程序中常见的错误。
SetTimeout不能保证函数将在设置的时间后被调用。相反,最好至少在这个时间过去之后再描述这个功能。
延迟函数
当setTimeout设置为0时会发生什么?
函数delay(){ setTimeout(()=console . log(' 0延迟超时!'),0) console.log('超时后')console.log('最后一个日志')} defer()您可能希望立即调用它,但事实并非如此。
执行结果:
延迟为0的timeoutlast logtimeout后!它将立即被推送到回调队列,但仍将等待调用堆栈为空后再执行。
用闭包缓存
记忆是一个缓存函数调用结果的过程。
例如,有一个函数add将两个数字相加。调用add(1,2)返回3。当使用相同的参数add(1,2)再次调用它时,这一次,不要重新计算,而是记住1 ^ 2是3的结果,并直接返回相应的结果。内存化可以提高代码的运行速度,是一个很好的工具。我们可以使用闭包来实现一个简单的memoize函数。
//缓存函数,接收函数const memoize=(func)={///缓存对象//key是参数,Values是结果const cache={ }//返回新函数//它记住了缓存对象func(闭包)//.args是返回的任意数量的参数(.args)={//将参数转换为字符串,这样我们就可以将其存储为const argstr=JSON。stringify(args)//如果已经存储,打印console.log('cache ',cache,cache[ArgStr])cache[ArgStr]=cache[ArgStr]| | func(.args)返回缓存[argStr]}}const add=memoize((a,B)=a b) console.log('第一个add call : ',add (1,2)) console.log('第二个add call ',add (1,2))执行结果:
Cache {} false第一个add call : 3Cache {'[1,2]' : 3} true第二个add call 3第一个add方法,cache对象为空,它调用我们的传入函数来获取值3。然后,它将参数/值键值对存储在缓存对象中。在第二次调用中,它已经存在于缓存中,并且被找到并返回。
对于add函数来说,有没有缓存似乎并不重要,甚至效率更低,但是可以为一些复杂的计算节省很多时间。这个例子不是一个完美的缓存例子,而是闭包的实际应用。
代码部署后可能存在的bug无法实时获知。为了事后解决这些bug,需要花费大量时间调试日志。顺便推荐一个不错的bug监控工具Fundebug。
以上就是本文的全部内容。希望对大家的学习有帮助,支持我们。通过示例代码详细介绍,对大家的学习或工作有一定的参考学习价值。
版权声明:初学者带你学习JavaScript引擎如何工作是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。