c语言中函数的运行机制,从变量和函数来看JS的执行机制(你真的理解JS的运行机制吗...

从一个小问题开始

昨天,我带的一个工作室的实习成员问了这样一个问题:

7bb687a34e16f6ec76ee18b210b614d9.png

这个问题我们可以简化一下,其实就是:function A() {console.log(username);

}

function B() {console.log('Uni');

}

B();复制代码

这位同学想问的是,在这种情况下会不会因为没有声明username而产生报错。

当然这个问题玩过前端的一眼就能看出来,正常输出Uni且不会报错。可是原理是什么呢?这中间到底发生了什么?为什么它不会有任何问题?

这篇文章将会从JS执行机制出发,更深层地探讨JS的基础问题。

声明和提升

首先,由浅入深,我们来回顾一下学习JS变量是遇到的知识,做一个小热身。我们先不讨论ES6的语法特性,这个部分我想留到这个系列的下篇文章进行探讨。

首先我们来看一段代码:console.log(username);   // undefined

var username = "Uni";复制代码

这段代码很简单,也就是我们常常说的变量提升。在JS的执行机制里面(这个机制是什么我等等讲)这段代码是这样的:var username = undefined;

console.log(username);    // undefined

username = "Uni";复制代码

我们再来看一个例子:test();      // Uni

function test() {console.log("Uni");

}复制代码

这里发生一个函数提升,在JS执行机制的眼里是这样的:function test() {console.log("Uni");

}

test();      // Uni复制代码

到这里都很简单对吧,我们再给点耐心看看另一个小例子:test();  var test = function() {      console.log("Uni");

}复制代码

这段代码会产生报错:

4d5442674782966003d753dcc6a8a2c1.png

报错的类型是TypeError。

通过上述例子,其实我更喜欢把提升叫做声明提升,不论是变量提升还是函数提升,它都只有是一个声明的时候才会发生提升。

接下来我们从JS代码执行的过程来看看提升。

JS代码的执行流程因为是对某一个点的知识进行讨论,与本篇文章讨论的知识无关的细节我会进行掩盖处理。

编译

初学JS的时候,我们大都可能听到:“JavaScript是一门解释型语言。”这样的话。但是JS事实上是一门编译语言,它是存在编译这个阶段的,只不过它的编译与传统的编译语言(譬如C语言)那样有些许不同。

在这里我不先从词法、LHS、RHS这些进行讨论,这些我会留在后面文章讨论作用域的时候拿出来仔细讨论。

我们首先要知道,JS执行流程的第一步是编译。通过编译,会生成两个部分:执行上下文和可执行代码。

执行上下文就是执行一段代码是的运行环境,执行上下文中包括:变量环境和词法环境。

8f2d0cfeb662ad400ae26cc1cd744af1.png

在编译时,JS引擎在变量环境中生成一个对象结构,我们称为变量对象。扫描代码,如果发现var声明的变量,会将其作为变量对象中的一个属性,并且赋予undefined。如果发现一个函数声明(通过function定义)的话,会将其定义存放在堆内存中,同时会在变量对象中创建一个属性,并且该属性的值是指向这个堆内存。

比如下面这段代码:var name = 'Uni';

console.log(name);

function Fun() {      var age = 20;      console.log(age);

}

Fun();复制代码

这一阶段的编译会生成类似于这样的变量对象VO = {      name: undefined,      Fun: function() {var age = 20;console.log(age);

}

}复制代码

而JS引擎会将除了声明以外的代码转化为字节码,也就是我们的可执行代码,一下是抽象写法,并不是实际上的:name = 'Uni';  console.log(name);

Fun();复制代码

执行

细心的同学应该会发现,我刚刚一直在有一重复一个词:当前阶段。这个词不是那么准确,但是能突出我想表达的。JS并不会只编译一次。不会在第一次编译阶段就讲所有的声明提出来。我们从上面的环境变量的例子也能看出来:VO = {      name: undefined,      Fun: function() {var age = 20;console.log(age);

}

}复制代码

我们Fun属性对应的值是存放于堆内存中的,我们可以看到函数内部的代码并没有做处理。

当我们可执行代码按顺序执行到我们的Fun()时,也就是调用Fun函数,JS引擎会找出这段函数代码,进行对这段函数的编译,从而生成这段函数的执行上下文和可执行代码。

调用栈

看到这里,我们可以知道,在一个JS代码开始运行的时候,进行第一次编译,会生成一个执行上下文,这个执行上下文我们叫做:全局执行上下文

而当调用一个函数的时候,函数体内的代码才会被编译,从而创建函数执行上下文。当函数中的可执行代码执行结束后,函数便会被销毁。

这是我们很自然的会想到,如果函数体里还有函数调用呢?那必然是会去找被调用的那个函数体然后进行代码编译(如果没有什么违规写法的话)。所以我们当前函数只有等自己函数体中的函数调用执行完了,它才能被销毁。我们来看段代码清晰一下思路:function A() {console.log('begin');

B();console.log('end');

}function B() {console.log('run');

}

A();复制代码

我们按照我们刚刚的思路来分析一下:

1、首先会生成一个全局的执行上下文,执行上下文中的变量对象是:VO:

A : {console.log('begin');

B();console.log('end');

}

B : {console.log('run');

}复制代码

生成可执行代码并执行。

2、调用我们的函数A,A的函数体中的代码进行编译,生成一个函数执行上下文:VO(A): {

}复制代码

生成函数A的可执行代码,并进行执行。

3、执行到B()时,调用B,对B的函数体进行编译,生成其函数执行上下文,生成其可执行代码并执行。

4、函数B执行结束,函数B销毁,继续执行函数A的可执行代码。

5、函数A执行结束,函数A销毁,回到全局的可执行代码中,发现无可执行代码,销毁全局(这里先忽略事件队列的相关内容)

我们简单模拟了一遍JS引擎的运行机制,那么JS引擎具体是如何管理这些执行上下文的呢?

JS引擎采用了一种名叫栈的数据结构对执行上下文进行管理,而这个结构我们称作调用栈。

学过数据结构的同学都知道,栈是一个先进后出的数据结构,它就像一个羽毛球筒,先放进去的我们会压在最底下,拿出来的话是最后才拿出来。

由于缺乏画图工具,我在网上找了一个很形象的图:

bf8085594bcdb0f970219b6245c45c2a.png

所以我们再来看看刚刚那段代码运行时,调用堆栈是怎么样的:

1、全局上下文入栈,被压到栈底

c44c06d7e43e2bfcd2c2214f06067b8d.png

2、调用函数A,函数A的执行上下文被压入栈

de72305f963244ca5ab06d7c156ae9b3.png

3、调用函数B,函数B的执行上下文被压入栈

aed4138fe7054d1326d5c715f82e1e4f.png

4、函数B执行完毕,函数B的执行上下文出栈。

5、函数A执行完毕,函数A的执行上下文出栈。

6、全局执行完毕,全局执行上下文出栈。

回到一开始

这个时候我们再来会看一开始那位实习的同学提出的问题,是不是更清晰为什么了?function A() {console.log(username);

}

function B() {console.log('Uni');

}

B();复制代码

我们函数A的函数体始终没有被编译,所以没有生成可执行代码,自然是不会被执行,所以也不会产生报错。

One more ting

其实这里有个小问题我留下了没有写,关于函数什么和函数表达式的,我打算再下篇深入讲解作用域时通过LHS和RHS来进行细讲。

此文章已经同步更新到我的个人微信公众号:想养猫的前端觉得不错的话,关注一下啦!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值