likes
comments
collection
share

JavaScript 在浏览器中的执行机制-变量提升

作者站长头像
站长
· 阅读数 11

既然说到了 JavaScript,那么就会绕不过去执行上下文,只有理解了执行上下文才能更好的理解 JavaScript 本身,比如变量提升、栈溢出、作用域和闭包等。

不过本部分不是专门讲解 JavaScript 的,所有不会对 JavaScript 本身做过多介绍,主要从 JavaScript 的执行顺序开始介绍一下 JavaScript 是怎样运行的。

首先先看一下下面的代码:

作为前端大家都知道上面的代码是同步的会顺序执行,那么输出应该是:

  • 当执行第一行时因为 showName 方法还没有定义应该会报错。
  • 同样执行第二行时因为变量 name 也没有定义所有也会报错。

但是如下图执行结果跟我们想的并不一样:

JavaScript 在浏览器中的执行机制-变量提升

第1行输出 “function showName run”,第2行输出 “undefined”,这和前面想象中的顺序执行有点不一样啊!

上面结果可以看出函数或者变量可以在定义之前使用,如果没有定义代码还能正常执行吗,可以参考下面代码去掉第三行 myName 的定义:

showName()
console.log(myname)
function showName() {
  console.log('function showName run')
}

JavaScript 在浏览器中的执行机制-变量提升

使用了未定义的变量——执行报错

从上面两段代码的执行结果来看,我们可以得出如下三个结论。

  1. 在执行过程中,若使用了未声明的变量,那么JavaScript执行会报错。
  2. 在一个变量定义之前使用它,不会出错,但是该变量的值会为undefined,而不是定义时的值。
  3. 在一个函数定义之前使用它,不会出错,且函数能正确执行。

第一个结论很好理解,因为变量没有定义,这样在执行JavaScript代码时,就找不到该变量,所以JavaScript会抛出错误。

但是对于第二个和第三个结论,就挺让人费解的,要解决这两个问题就要明白接下来我们要介绍的内容-变量提升。

变量提升

在正式了解变量提升之前,我们需要先理解两个 JavaScript 概念声明和赋值

这段代码可以这样拆解看:

JavaScript 在浏览器中的执行机制-变量提升

上面说明了变量的声明和赋值,接下来说一下函数的声明和赋值:

function foo(){
  console.log('foo')
}

var bar = function(){
  console.log('bar')
}

第一个函数foo是一个完整的函数声明,也就是说没有涉及到赋值操作;第二个函数是先声明变量bar,再把function(){console.log('bar')}赋值给bar。为了直观理解,你可以参考下图:

JavaScript 在浏览器中的执行机制-变量提升

通过上面我们了解了变量和函数的声明和赋值了,接下来说一说什么是变量提升。

所谓的变量提升就是指 JavaScript 代码在执行过程中, JavaScript 引擎把变量和函数的声明部分提升到代码开头的”行为“。变量提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined

具体变化如下代码:

可以通过下图更直观的感受:

JavaScript 在浏览器中的执行机制-变量提升

从图中可以看出,对原来的代码主要做了两处调整:

通过这两步,就可以实现变量提升的效果。你也可以执行这段模拟变量提升的代码,其输出结果和第一段代码应该是完全一样的。

通过这段模拟的变量提升代码,明白了可以在定义之前使用变量或者函数的原因——函数和变量在执行之前都提升到了代码开头。

let和const会变量提升吗?

我们执行下面代码进行尝试:

JavaScript 在浏览器中的执行机制-变量提升

上图的报错为:ReferenceError: Cannot access 'variable' before initialization。和未定义的的报错不同,未定义报错如下

JavaScript 在浏览器中的执行机制-变量提升 由此可见,用 var 定义的变量可以在声明之前被访问而不报错,但是用 let 或 const 定义的变量却不行。

由 let 或 const 定义的变量提升时不会默认初始化,所以在声明之前访问会报错:ReferenceError: Cannot access 'variable' before initialization

然而由 var 定义的变量提升时被初始化为默认值 undefined,所以在声明之前访问会得到 undefined。 这里面就是涉及到了一个概念 暂时性死区,let/const 定义的变量被提升却无法正常访问,是因为存在暂时性死区(Temporal Dead Zone)

JavaScript 代码的执行顺序

通过上面介绍变量提升,你可能会觉得变量和函数会在物理层面中被移动到代码最前面,就是我们图中展示的那样,但是,实际上变量和函数声明在代码里的位置并不会改变,而是在编译阶段被 JavaScript 引擎放入到内存中,你没看错,一段 JavaScript 代码在被执行前需要 JavaScript 引擎编译,编译完成后才会被执行,如下图所示:

JavaScript 在浏览器中的执行机制-变量提升

1.编译阶段

编译和变量提升有什么关系呢?我们可以从下面两部分代码来看:

第一部分:变量提升部分的代码。

var myName = undefined
function showName() {
    console.log('function showName run');
}

第二部分:执行部分的代码。

可以把JavaScript的执行流程细化,如下图所示:

JavaScript 在浏览器中的执行机制-变量提升

从上图可以看出,输入一段代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。

执行上下文是JavaScript执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如this、变量、对象以及函数等。

可以用下面的伪代码简单的理解环境变量对象:

VariableEnvironment:
     myname -> undefined, 
     showName ->function : {console.log(myname)

接下来看一下生成环境变量的过程:

  • 第1行和第2行,由于这两行代码不是声明操作,所以JavaScript引擎不会做任何处理;
  • 第3行,由于这行是经过var声明的,因此JavaScript引擎将在环境对象中创建一个名为 myName的属性,并使用 undefined 对其初始化;
  • 第4行,JavaScript引擎发现了一个通过 function 定义的函数,所以它将函数定义存储到堆(HEAP)中,并在环境对象中创建一个showName的属性,然后将该属性值指向堆中函数的位置。

这样就生成了变量环境对象。接下来JavaScript引擎会把声明以外的代码编译为字节码,可以类比如下的伪代码:

好了,现在有了执行上下文和可执行代码了,那么接下来就到了执行阶段了。

2. 执行阶段

JavaScript引擎开始执行“可执行代码”,按照顺序一行一行地执行。下面我们就来一行一行分析下这个执行过程:

代码中出现相同的变量或者函数怎么办?

现在你已经知道了,在执行一段JavaScript代码之前,会编译代码,并将代码中的函数和变量保存到执行上下文的变量环境中,那么如果代码中出现了重名的函数或者变量,JavaScript引擎会如何处理?

我们先看下面这样一段代码:

分析这两次调用打印出来的值是什么吗?

完整执行流程:

综上所述,一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。

总结

好了,今天就到这里,下面我来简单总结下今天的主要内容:

  • JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译。
  • 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined;在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
  • 如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。

以上就是今天所讲的主要内容,当然,学习这些内容并不是让你掌握一些 JavaScript 小技巧,其主要目的是让你清楚JavaScript的执行机制:先编译,再执行