目录
在了解JavaScript的执行机制之前我们先来了解一下变量提升,变量提升是什么?为什么需要?
接下来我们来理解一下JavaScript的变量类型——var缺陷
所以ES6 是如何解决变量提升带来的缺陷——ES6 引入了 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 标记出来了。
代码中出现相同的变量或者函数怎么办?
一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。
基于作用域链和词法环境,我们来重新认识闭包(明天再来)