浏览器中的JavaScript的执行机制

目录

JavaScript代码执行的顺序是什么呢?

在了解JavaScript的执行机制之前我们先来了解一下变量提升,变量提升是什么?为什么需要?

编译阶段

说到这里我们来学习一下执行上下文

JavaScript中使用什么方法跟踪执行上下文?

讲到这里需要补充什么是栈?

当栈中容量不足,会发生什么?那为什么会出现这个问题呢?

接下来我们来理解一下JavaScript的变量类型——var缺陷

所以ES6 是如何解决变量提升带来的缺陷——ES6 引入了 let 和 const 关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域。

let与const有什么不同?

JavaScript 是如何支持块级作用域的

在同一段代码中,ES6 是如何做到既要支持变量提升的特性,又要支持块级作用域的呢?

执行阶段

说到在变量环境中查找自定义的变量和函数,我们就想到作用域,JavaScript 引擎在过全局作用域和函数级作用域如何查找变量的值的呢?

 foo 函数调用的 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?要回答这个问题,我们需要知道什么是词法作用域

JavaScript 引擎在过块级作用域如何查找变量的值的呢?

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

基于作用域链和词法环境,我们来重新认识闭包(明天再来)


 

JavaScript代码执行的顺序是什么呢?

JavaScript 代码的执行流程

在了解JavaScript的执行机制之前我们先来了解一下变量提升,变量提升是什么?为什么需要?

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

JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译。

变量提升主要发生在编译阶段,

编译阶段

在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined。

我们要知道是通过执行上下文跟踪变量的,执行上下文是 JavaScript 执行一段代码时的运行环境

经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

说到这里我们来学习一下执行上下文

  • 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
  • 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
  • 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。

JavaScript中使用什么方法跟踪执行上下文?

JavaScript是单线程的执行模型,在某特定时刻只能执行特定的代码。一旦发生函数调用,必须停止当前的执行上下文,创建出新的函数执行上下文俩执行函数。也就是说遇到一个新的先执行新的,专业一点讲就是后进先出。这里可以想到数据结构的栈,所以我们使用的跟踪方法是执行上下文栈,简称调用栈。是 JavaScript 的调用栈

调用栈是怎么变化的?通过一段代码我们来了解一下


var a = 2
function add(b,c){
  return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return  a+result+d
}
addAll(3,6)

第一步,创建全局上下文,并将其压入栈底。

从图中你也可以看出,变量 a、函数 add 和 addAll 都保存到了全局上下文的变量环境对象中。全局执行上下文压入到调用栈后,JavaScript 引擎便开始执行全局代码了。首先会执行 a=2 的赋值操作,执行该语句会将全局上下文变量环境中 a 的值设置为 2。设置后的全局上下文的状态如下图所示:

接下来,第二步是调用 addAll 函数。当调用该函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中,如下图所示

addAll 函数的执行上下文创建好之后,便进入了函数代码的执行阶段了,这里先执行的是 d=10 的赋值操作,执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成了 10。

然后接着往下执行,第三步,当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈,如下图所示:

当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9。如下图所示:

紧接着 addAll 执行最后一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。最终如下图所示:

讲到这里需要补充什么是栈?

关于栈,你可以结合这么一个贴切的例子来理解,一条单车道的单行线,一端被堵住了,而另一端入口处没有任何提示信息,堵住之后就只能后进去的车子先出来,这时这个堵住的单行线就可以被看作是一个栈容器,车子开进单行线的操作叫做入栈,车子倒出去的操作叫做出栈。

当栈中容量不足,会发生什么?那为什么会出现这个问题呢?

调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。

这是因为当 JavaScript 引擎开始执行这段代码时,它首先调用函数 division,并创建执行上下文,压入栈中;然而,这个函数是递归的,并且没有任何终止条件,所以它会一直创建新的函数执行上下文,并反复将其压入栈中,但栈是有容量限制的,超过最大数量后就会出现栈溢出的错误。

接下来我们来理解一下JavaScript的变量类型——var缺陷

前面说到变量提升,那么变量提升所带来的问题:

1. 变量容易在不被察觉的情况下被覆盖掉

2. 本应销毁的变量没有被销毁

所以ES6 是如何解决变量提升带来的缺陷——ES6 引入了 let 和 const 关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域。

所以ES6以后JavaScript的变量类型有var,let,const

let与const有什么不同?

使用 let 关键字声明的变量是可以被改变的,而使用 const 声明的变量其值是不可以被改变的。

JavaScript 是如何支持块级作用域的

第一步是编译并创建执行上下文

函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。在函数的作用域块内部,通过 let 声明的变量并没有被存放到词法环境中。

第二步继续执行代码

词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出

具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。

在同一段代码中,ES6 是如何做到既要支持变量提升的特性,又要支持块级作用域的呢?

块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。

执行阶段

在代码执行阶段,JavaScript 引擎会从变量环境(执行上下文)中去查找自定义的变量和函数。

词法环境是JavaScript内部用来跟踪标识符与特定变量之间的映射关系。

使用词法环境来跟踪变量的作用域,词法环境是JavaScript作用域的内部实现机制,也称作用域。

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

说到在变量环境中查找自定义的变量和函数,我们就想到作用域,JavaScript 引擎在过全局作用域和函数级作用域如何查找变量的值的呢

当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。

其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。

      ​​​​​​​

从图中可以看出,bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链。

foo 函数调用的 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?要回答这个问题,我们需要知道什么是词法作用域

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。

这是因为根据词法作用域,foo 和 bar 的上级作用域都是全局作用域,所以如果 foo 或者 bar 函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找。也就是说,词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

JavaScript 引擎在过块级作用域如何查找变量的值的呢

会现在当前执行的函数的执行上下文查找,先找词法环境再找变量环境,如果没有就到继续在 outer 所指向的执行上下文中查找,也就是全局执行上下文,同理先找词法环境再找变量环境

现在是执行到 bar 函数的 if 语块之内,需要打印出来变量 test,那么就需要查找到 test 变量的值,其查找过程我已经在上图中使用序号 1、2、3、4、5 标记出来了。

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

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

基于作用域链和词法环境,我们来重新认识闭包(明天再来)

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值