JavaScript在浏览器中的执行机制

 更新时间:2023年09月12日 08:41:42   作者:托儿所夜十三  
既然说到了JavaScript,那么就会绕不过去执行上下文,只有理解了执行上下文才能更好的理解JavaScript本身,不过本部分不是专门讲解 JavaScript的,主要从JavaScript的执行顺序开始介绍一下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执行机制的资料请关注脚本之家其它相关文章!

相关文章

最新评论