js底层,小白也能看懂的js执行机制

756 阅读6分钟

前言

JavaScript,作为一种广泛应用于网页开发、服务器端编程以及各类前端框架中的动态编程语言,其执行机制是其高效、灵活和强大特性的基础。理解 JavaScript 的执行机制,对于开发者来说至关重要,这不仅有助于优化代码性能,还能有效避免一些常见的陷阱和错误。

一,声明提升

showName()
console.log(myname);

var myname = 'cmx'
function showName() {
    console.log('函数showName被执行')
}

在这段代码当中,在使用showName()方法和输出myname变量之前,我们还并未定义showName方法,和变量myname,所以按照代码从上往下的执行方式,这段代码将会报错,那我们看看代码的运行结果吧

5.png 结果是代码正常运行!函数showname()可以正常使用定义的方法,变量myname也已经被定义过了,如果面试官问你为什么,相信很多小伙伴都知道声明提升(在代码执行过程,会将声明的变量往上提升,会将声明的函数集体提升),下面是v8引擎看到的样子

var myname
function showName() {
    console.log('函数showName');
}
showName()
console.log(myname);
myname = 'cmx'

那面试官再往下问;为什么存在声明提升这个概念。这时候我们就内心一万个草泥马了,这个是js的语言特性啊,我怎么知道为什么,这时候我们就要来了解js的执行机制了

二,执行上下文

首先,要明确的是,JavaScript代码在执行前,会先经过编译阶段。这意味着,代码并不是一读入就立即执行,而是先被解析和准备,然后才进入执行阶段。这个准备过程,就是为代码创建执行上下文。 执行上下文,可以理解为代码执行的舞台或环境。每个函数或全局代码块都有自己独立的执行上下文,下面是执行上下文的具体结构

6.png 可以看到,在执行上下文中将空间规划成三个板块,分别为变量环境,词法环境,整理后可执行的代码,变量环境当中存放的就是我们声明的变量,在这个执行上下文空间内,js引擎会读取,编译代码。那么执行上下文中js引擎的编译过程是怎么样的呢,我们接着往下看

三,调用栈

1,什么是调用栈

调用栈是 JavaScript 引擎用于管理函数调用的一种数据结构。每当一个函数被调用时,它的执行上下文(包括变量、作用域链等)就会被压入调用栈中。当函数执行完毕后,它的执行上下文就会被弹出调用栈。如果调用栈中的函数过多,导致栈空间耗尽,就会抛出“栈溢出”错误

首先,我们要确认在js引擎当中是否有栈这个结构,我们可以通过下面代码来得知

function text() {
    text()
}                                                                           
text()

这是个奇怪的代码,怎么在text()方法里调用text()方法呢,这样就会形成无限循环,那我们来看看代码的输出结果

7.png

代码报错了,最大的栈空间超出了最大的栈空间,也就是“栈溢出”,由此可见js引擎里面一定有一个栈空间,那么这个js引擎当中的栈叫什么名字呢,js引擎当中的栈叫调用栈 。

2,调用栈如何存储执行上下文

看下面一段代码

var a = 5
function add(){
    var b = 10
    return a+b
}

首先,我们知道add()方法返回的结果为15,因为在add()函数内badd函数内的作用域,a不是add()函数内的作用域,但是a是全局作用域,在add()方法中找不到的时候,函数作用域是可以访问全局作用域的,而全局作用域不可以访问函数作用域,那我们知道为什么函数作用域是可以访问全局作用域吗。这就要看调用栈的存储方式了

在这段代码中,全局代码会先解析,形成全局执行上下文,并存入调用栈当中

8.png

然后执行全局代码块时碰到函数代码块,这时add()函数代码块又会被解析,形成add()函数上下文并存入调用栈当中

9.png

add函数上下文中执行到a时,由于在add函数上下文中找不到a,所以他会沿着调用栈往下找,在全局上下文中找a,因此在我们看来,函数作用域可以访问全局作用域,但是全局作用域不能访问函数作用域

四,编译执行过程

我们以下面代码为例

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

首先,我们要先创建一个全局执行上下文,找变量声明和函数声明,将变量声明的变量名作为key,值为undefiend,将函数声明的函数名作为key,值为函数体,全局中有一个 var a为变量声明,有一个function fn()为函数声明,所以调用栈内添加全局执行上下文,变量环境内添加声明变量,下面是第一步结构

10.png

声明结束后开始执行代码,a=1,然后执行fn()函数调用栈里面创建函数fn()执行上下文,下面是第二步结构

11.png 然后就开始找函数fn()中的变量声明,有var a ,var b,这是第三步结构

12.png 接下来还是在编译阶段,将形参a和实参3进行统一,即实参会将形参覆盖掉,这是第四步结构

13.png 在编译的最后阶段才会找函数声明,这里由于函数啊a()与变量a重名,所以会将a覆盖掉,变为函数体,下面是第五步结构

14.png 至此函数fn()编译完成,开始执行a=2,b=a,输出a,a=2将前面a()函数声明覆盖掉了,所以最后的输出为2,下面是最后一步和它的输出结果

15.png

16.png 执行完后,执行上下文并不会在调用栈里面停留,而是会被销毁,因此在执行完后,调用栈内又会变的空空如也。

总结

1, 创建执行上下文对象

2,找形参和变量声明,将形参和声明的变量名作为key,值为undefiend

3,统一形参和实参的值(全局没有改步骤)

4,找函数声明,将函数名作为key,值为函数体

5,执行代码

学习检验

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)

分析出这段代码的输出结果