JavaScript在浏览器中的执行机制
既然说到了 JavaScript,那么就会绕不过去执行上下文,只有理解了执行上下文才能更好的理解 JavaScript 本身,比如变量提升、栈溢出、作用域和闭包等。
不过本部分不是专门讲解 JavaScript 的,所有不会对 JavaScript 本身做过多介绍,主要从 JavaScript 的执行顺序开始介绍一下 JavaScript 是怎样运行的。
首先先看一下下面的代码:
showName() console.log(myname) var myname = '掘金' function showName() { console.log('function showName run') }
作为前端大家都知道上面的代码是同步的会顺序执行,那么输出应该是:
- 当执行第一行时因为
showName
方法还没有定义应该会报错。 - 同样执行第二行时因为变量
name
也没有定义所有也会报错。
但是如下图执行结果跟我们想的并不一样:
第1行输出 “function showName run”,第2行输出 “undefined”,这和前面想象中的顺序执行有点不一样啊!
上面结果可以看出函数或者变量可以在定义之前使用,如果没有定义代码还能正常执行吗,可以参考下面代码去掉第三行 myName
的定义:
showName() console.log(myname) function showName() { console.log('function showName run') }
使用了未定义的变量——执行报错
从上面两段代码的执行结果来看,我们可以得出如下三个结论。
- 在执行过程中,若使用了未声明的变量,那么JavaScript执行会报错。
- 在一个变量定义之前使用它,不会出错,但是该变量的值会为undefined,而不是定义时的值。
- 在一个函数定义之前使用它,不会出错,且函数能正确执行。
第一个结论很好理解,因为变量没有定义,这样在执行JavaScript代码时,就找不到该变量,所以JavaScript会抛出错误。
但是对于第二个和第三个结论,就挺让人费解的,要解决这两个问题就要明白接下来我们要介绍的内容-变量提升。
变量提升
在正式了解变量提升之前,我们需要先理解两个 JavaScript 概念声明和赋值。
var myName = "掘金"
这段代码可以这样拆解看:
var myName //声明部分 myName = "掘金" //赋值部分
上面说明了变量的声明和赋值,接下来说一下函数的声明和赋值:
function foo(){ console.log('foo') } var bar = function(){ console.log('bar') }
第一个函数foo是一个完整的函数声明,也就是说没有涉及到赋值操作;第二个函数是先声明变量bar,再把function(){console.log('bar')}赋值给bar。为了直观理解,你可以参考下图:
通过上面我们了解了变量和函数的声明和赋值了,接下来说一说什么是变量提升。
所谓的变量提升就是指 JavaScript 代码在执行过程中, JavaScript 引擎把变量和函数的声明部分提升到代码开头的”行为“。变量提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined
。
具体变化如下代码:
/* * 变量提升部分 */ // 把变量 myname提升到开头, // 同时给myname赋值为undefined var myname = undefined // 把函数showName提升到开头 function showName() { console.log('function showName run'); } /* * 可执行代码部分 */ showName() console.log(myname) // 去掉var声明部分,保留赋值语句 myname = '掘金'
可以通过下图更直观的感受:
从图中可以看出,对原来的代码主要做了两处调整:
- 第一处是把声明的部分都提升到了代码开头,如变量
myName
和函数showName
,并给变量设置默认值undefined
; - 第二处是移除原本声明的变量和函数,如
var myName = '掘金'
的语句,移除了 var 声明,整个移除showName
的函数声明。
通过这两步,就可以实现变量提升的效果。你也可以执行这段模拟变量提升的代码,其输出结果和第一段代码应该是完全一样的。
通过这段模拟的变量提升代码,明白了可以在定义之前使用变量或者函数的原因——函数和变量在执行之前都提升到了代码开头。
let和const会变量提升吗?
我们执行下面代码进行尝试:
console.log(myName) const myName = '掘金'
上图的报错为:ReferenceError: Cannot access 'variable' before initialization。和未定义的的报错不同,未定义报错如下
console.log(myName1) const myName = '掘金'
由此可见,用 var 定义的变量可以在声明之前被访问而不报错,但是用 let
或 const
定义的变量却不行。
由 let
或 const
定义的变量提升时不会默认初始化,所以在声明之前访问会报错:ReferenceError: Cannot access 'variable' before initialization。
然而由 var
定义的变量提升时会被初始化为默认值 undefined
,所以在声明之前访问会得到 undefined
。 这里面就是涉及到了一个概念 暂时性死区
,let
/const
定义的变量被提升却无法正常访问,是因为存在暂时性死区(Temporal Dead Zone) 。
JavaScript 代码的执行顺序
通过上面介绍变量提升,你可能会觉得变量和函数会在物理层面中被移动到代码最前面,就是我们图中展示的那样,但是,实际上变量和函数声明在代码里的位置并不会改变,而是在编译阶段被 JavaScript 引擎放入到内存中,你没看错,一段 JavaScript 代码在被执行前需要 JavaScript 引擎编译,编译完成后才会被执行,如下图所示:
1.编译阶段
编译和变量提升有什么关系呢?我们可以从下面两部分代码来看:
第一部分:变量提升部分的代码。
var myName = undefined function showName() { console.log('function showName run'); }
第二部分:执行部分的代码。
showName() console.log(myName) myName = '掘金'
可以把JavaScript的执行流程细化,如下图所示:
从上图可以看出,输入一段代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。
执行上下文是JavaScript执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如this、变量、对象以及函数等。
可以用下面的伪代码简单的理解环境变量对象:
VariableEnvironment: myname -> undefined, showName ->function : {console.log(myname)
接下来看一下生成环境变量的过程:
showName() console.log(myName) var myName = '掘金' function showName() { console.log('function showName run'); }
- 第1行和第2行,由于这两行代码不是声明操作,所以JavaScript引擎不会做任何处理;
- 第3行,由于这行是经过var声明的,因此JavaScript引擎将在环境对象中创建一个名为
myName
的属性,并使用undefined
对其初始化; - 第4行,JavaScript引擎发现了一个通过 function 定义的函数,所以它将函数定义存储到堆(HEAP)中,并在环境对象中创建一个showName的属性,然后将该属性值指向堆中函数的位置。
这样就生成了变量环境对象。接下来JavaScript引擎会把声明以外的代码编译为字节码,可以类比如下的伪代码:
showName() console.log(myname) myname = '掘金'
好了,现在有了执行上下文和可执行代码了,那么接下来就到了执行阶段了。
2. 执行阶段
JavaScript引擎开始执行“可执行代码”,按照顺序一行一行地执行。下面我们就来一行一行分析下这个执行过程:
- 当执行到
showName
函数时,JavaScript 引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以 JavaScript 引擎便开始执行该函数,并输出“函数showName
被执行”结果。 - 接下来打印
myName
信息,JavaScript 引擎继续在变量环境对象中查找该对象,由于变量环境存在myName
变量,并且其值为undefined
,所以这时候就输出undefined
。 - 接下来执行第3行,把“掘金”赋给
myName
变量,赋值后变量环境中的myname
属性值改变为“掘金”,变量环境如下所示:
VariableEnvironment: myname -> "掘金", showName ->function : {console.log(myName)
代码中出现相同的变量或者函数怎么办?
现在你已经知道了,在执行一段JavaScript代码之前,会编译代码,并将代码中的函数和变量保存到执行上下文的变量环境中,那么如果代码中出现了重名的函数或者变量,JavaScript引擎会如何处理?
我们先看下面这样一段代码:
function showName() { console.log('沸点'); } showName(); function showName() { console.log('掘金'); } showName();
分析这两次调用打印出来的值是什么吗?
完整执行流程:
- 首先是编译阶段。遇到了第一个
showName
函数,会将该函数体存放到变量环境中。接下来是第二个showName
函数,继续存放至变量环境中,但是变量环境中已经存在一个showName
函数了,此时,第二个showName
函数会将第一个showName
函数覆盖掉。这样变量环境中就只存在第二个showName
函数了。 - 接下来是执行阶段。先执行第一个
showName
函数,但由于是从变量环境中查找showName
函数,而变量环境中只保存了第二个showNam
e函数,所以最终调用的是第二个函数,打印的内容是“掘金”。第二次执行showName函数也是走同样的流程,所以输出的结果也是“掘金”。
综上所述,一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。
总结
好了,今天就到这里,下面我来简单总结下今天的主要内容:
- JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译。
- 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为
undefined
;在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。 - 如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。
以上就是今天所讲的主要内容,当然,学习这些内容并不是让你掌握一些 JavaScript 小技巧,其主要目的是让你清楚JavaScript的执行机制:先编译,再执行。
以上就是JavaScript在浏览器中的执行机制的详细内容,更多关于JavaScript执行机制的资料请关注脚本之家其它相关文章!
相关文章
JavaScript实现echarts水球图百分比展示大屏可视化
这篇文章主要为大家介绍了JavaScript实现echarts水球图百分比展示大屏可视化示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2022-10-10
最新评论