如何撬开js的大门—从js执行机制聊起

266 阅读5分钟

一.前言

在上篇文章中,我们了解到了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 = '张三'

即输出结果显示:image.png

这是因为在js中代码的执行顺序并不总是按照它在代码中出现的顺序来执行,会按照特定的规则(也就是js执行机制)来执行,也就是说会存在变量和函数的声明会被提升。那么为什么会存在变量提升和函数提升呢?这就不得不引出执行上下文这个概念了。

二.执行上下文

执行上下文(Execution Context)是用来执行js代码的环境的抽象概念。通常包含全局执行上下文函数执行上下文。每一段js代码运行时,这段代码会按特定的规则存放在一包括存中,执行上下文包含了代码执行所需的所有信息,包括变量、函数声明、作用域链等。执行上下文存在一个变量环境和一个词法环境,(变量环境用来存放所声明的变量和声明的函数,ES6中专门用词法环境存放let和const), 最后梳理成可执行的代码。如下图。 image.png

例如全局执行上下文和函数执行上下文

image.png

三.调用栈及编译过程

调用栈就是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函数执行上下文,执行完后销毁出栈,再执行全局执行上下文,最后销毁,栈空,过程如下所示;

ad33cd80ae7226f06268d649dfe9648.png

image.png

3.png

599abfc015e15b6d24fcd1e4da3afb0.png

4.png

0d62f01878e9aeb1e337307f340d015.jpg

再看如下代码,推测其输出值

var a = 1
function fn(a) {
  var a = 2
  function a() { }
  var b = a
  console.log(a);

}
fn(3)

当fn执行上下文编译时,会先形参与实参匹配,然后再声明函数体 95d457fffaff43fdd58b500db910dd6.png

image.png

通过上面例子可总结出编译过程

  1. 创建执行上下文对象
  2. 找形参和变量声明,将形参和声明的变量名作为key,值为underfined
  3. 统一形参和实参的值
  4. 找函数声明,将函数名作为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)

输出结果为:

image.png

四.总结

  • 调用栈与执行上下文
  1. 当js代码开始执行时,首先创建全局执行上下文并推入调用栈。
  2. 当一个函数被调用时,一个新的函数执行上下文被创建并推入调用栈的顶部。
  3. 当函数执行完成后,它的执行上下文会从调用栈中弹出(销毁),接着返回到之前的执行上下文。
  4. 如果调用栈为空,则js引擎可以退出或者执行其他任务。
  • 编译过程
  1. 创建执行上下文对象
  2. 找形参和变量声明,将形参和声明的变量名作为key,值为underfined
  3. 统一形参和实参的值
  4. 找函数声明,将函数名作为key,值为函数体
  • 执行流程
  1. 编译总是发生在执行前一刻
  2. 全局和函数体的编译会生成执行上下文存入调用栈
  3. 当一个函数执行完毕后,它的执行上下文就会被销毁