一.前言
在上篇文章中,我们了解到了js作用域的相关知识,知道了var存在变量提升,内层作用域可以访问外层,而外层不能访问内层,但是这是为什么呢?这就需要理解js的执行机制了,那么我们先看以下代码,猜想其会输出什么;
showName()
console.log(myname);
var myname = '张三'
function showName() {
console.log('函数showName被执行');
}
而在v8引擎里上面代码会长成下面这样
var myname
function showName() {
console.log('函数showName被执行');
}
showName()
console.log(myname);
myname = '张三'
即输出结果显示:
这是因为在js中代码的执行顺序并不总是按照它在代码中出现的顺序来执行,会按照特定的规则(也就是js执行机制)来执行,也就是说会存在变量和函数的声明会被提升。那么为什么会存在变量提升和函数提升呢?这就不得不引出执行上下文这个概念了。
二.执行上下文
执行上下文(Execution Context)是用来执行js代码的环境的抽象概念。通常包含全局执行上下文和函数执行上下文。每一段js代码运行时,这段代码会按特定的规则存放在一包括存中,执行上下文包含了代码执行所需的所有信息,包括变量、函数声明、作用域链等。执行上下文存在一个变量环境和一个词法环境,(变量环境用来存放所声明的变量和声明的函数,ES6中专门用词法环境存放let和const), 最后梳理成可执行的代码。如下图。
例如全局执行上下文和函数执行上下文
三.调用栈及编译过程
调用栈就是v8引擎用来管理函数之间的调用关系的一种结构。编译总是发生在执行前一刻,在全局上下文中调用执行一个函数时,程序就进入该被调用函数内,此时v8引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行栈顶部。浏览器总是执行位于执行栈顶部的当前执行上下文,一旦执行完毕,该执行上下文就会从栈顶部消除,然后进入下面的执行上下文。
var a = 2
function add(){
var b = 10
return a + b
}
add()
再如上例子,该怎么去访问a的值呢?都知道内层作用域访问不到去外层作用域找,那么是为什么呢?这就需要知道执行上下文的的执行顺序(先编译再执行)以及调用栈。举个如下例子;
var a = 2
function add() {
return b + c
}
function addAll(b, c) {
var d = 10
var result = add(b, c)
return a + result + d
}
addAll(3, 6)
代码首先在全局执行上下文里面编译执行,然后全局执行上下文入栈,先编译再执行,编译过程就是找出声明的变量,全局执行上下文声明了变量a,函数add(),函数addAll(),然后再执行,执行过程中调用了函数addAll函数,然后将addAll函数执行上下文入栈,编译addAll执行上下文,然后执行过程中调用了add函数,则add函数入栈,由于浏览器总是执行位于栈顶的执行上下文,所以先执行add函数执行上下文,执行完后消除add函数执行上下文,然后执行addAll函数执行上下文,执行完后销毁出栈,再执行全局执行上下文,最后销毁,栈空,过程如下所示;
再看如下代码,推测其输出值
var a = 1
function fn(a) {
var a = 2
function a() { }
var b = a
console.log(a);
}
fn(3)
当fn执行上下文编译时,会先形参与实参匹配,然后再声明函数体
通过上面例子可总结出编译过程:
- 创建执行上下文对象
- 找形参和变量声明,将形参和声明的变量名作为key,值为underfined
- 统一形参和实参的值
- 找函数声明,将函数名作为key,值为函数体
最后可以细品下面代码;
function fn(a) {
console.log(a);
var a = 123
console.log(a);
function a() { }
console.log(a);
var b = function () { }
console.log(b);
function d() { }
var d = a
console.log(d);
}
fn(1)
输出结果为:
四.总结
- 调用栈与执行上下文
- 当js代码开始执行时,首先创建全局执行上下文并推入调用栈。
- 当一个函数被调用时,一个新的函数执行上下文被创建并推入调用栈的顶部。
- 当函数执行完成后,它的执行上下文会从调用栈中弹出(销毁),接着返回到之前的执行上下文。
- 如果调用栈为空,则js引擎可以退出或者执行其他任务。
- 编译过程:
- 创建执行上下文对象
- 找形参和变量声明,将形参和声明的变量名作为key,值为underfined
- 统一形参和实参的值
- 找函数声明,将函数名作为key,值为函数体
- 执行流程:
- 编译总是发生在执行前一刻
- 全局和函数体的编译会生成执行上下文存入调用栈
- 当一个函数执行完毕后,它的执行上下文就会被销毁