1
前言

我们都知道JS是按顺序执行的,那好的,我们来看下面这一段代码。

print();
console.log(str);
var str = 'hello world!';
function print() {
    console.log(str)
}

如果你写过js,那就知道,这段代码不会报错,会正常输出
1.png
如果你不能理解,不要着急,我们删除var str = 'hello world',再执行:

print();
console.log(str);
function print() {
    console.log(str)
}

2.png
报错,说好的顺序执行呢?不要着急,要了解js的运行方式,我们要先了解变量提升。

变量提升(Hosting)

在介绍变量提升之前,我们先通过以下两个例子来讲解JS中的声明和赋值。
在es6之前,JS都是通过var来声明变量。

var str = 'hello world!';

这一句等价于

var str; // 变量声明
str = 'hello world!'; // 赋值

接下来我们在看一下函数声明

function print() {
    console.log(str)
}

这个是一个完整的函数声明,没毛病。

var print = function () {
  console.log(str)  
}

这段代码是一个声明变量和赋值的过程,相当于

var print; // 变量声明
print = function () { // 赋值
    console.log(str)
}

所谓的变量提升,是指JavaScript在执行的过程中,JavaScript引擎把变量提升和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置值为undefined。

模拟变量提升
3.png

JS 执行流程

从概念的字面上来看,“变量提升”意味着变量和函数的声明会在物理层移动到代码的最前面,正如我们所模拟的那样。但其实是:变量和函数声明在代码里的位置是不会改变的,而且在编译阶段被JavaScript引擎放入内存中。JS代码编译完成之后才会进入执行阶段。
大致流程如下图:
4.png
接下来详细介绍一下流程,还是以刚刚的例子为例:

  • 变量提升阶段的代码
var str = undefined;
function print() {
    console.log(str);
}
  • 执行阶段的代码
print();
console.log(str);
str = 'hello world!';

JavaScript执行流程图
5.png
从上图可以看出,输入一段JS代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。

执行上下文

是指JS执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间的用到变量如this、变量、对象、函数等。

在执行上下文中存在一个变量环境的对象(Variable Environment),该对象中保存了变量提升的内容,比如上面代码中变量str和函数print都保存在该对象中。

类似这样:

Variable Environment:
    str -> undefined,
    print -> function () { console.log(str) };

接下来逐行分析代码:

print(); // 1
console.log(str); // 2
var str = 'hello world!'; // 3
function print() { // 4
    console.log(str)
}
编译阶段
  • 1和2,不是声明操作,JS引擎不做任何处理
  • 3,通过var 声明变量,所以JS引擎在环境对象中创建一个名为str的属性,并初始化为undefined
  • 4,这是一个函数声明。JS引擎发现了一个通过function定义的函数,所以JS引擎会将函数定义部分存到堆(HEAP)中,并在环境对象中创建一个print属性,并将print的值指向堆中函数的位置。

这样就生成了变量环境对象,接着JS引擎会把声明之外的代码编译成字节码(可执行代码),也就是下面这一段模拟代码:

print();
console.log(str);
str = 'hello world!';
执行阶段

JS引擎开始执行“可执行代码”,按照顺序一行一行的执行,过程如下:

  • 执行到print函数时,JS引擎变开始在变量环境对象中寻找该函数,由于变量环境对象中print存在该函数的引用,所以JS引擎便开始执行该函数,在执行过程发现,函数用了str变量,所以JS引擎继续在变量环境中寻找str变量,此时str在变量环境对象中的值为undefined, 所以输出undefined.(这里细节,后续再补充)
  • console.log(str), JS引擎继续在变量环境对象中查找str,此时str在变量环境对象中的值为undefined, 所以输出undefined.
  • str = 'hello world!'; 这是一个赋值操作,把'hello world!'赋值给str, 赋值结束后变量环境对象变成:
Variable Environment:
    str -> 'hello world!',
    print -> function () { console.log(str) };

整个流程大致差不多这样,但是其实细节还是很复杂的,后续有空再继续补充。

有个问题,如果代码中存在多个相同的变量和函数时怎么办

如下面这段代码,建议小伙伴自己先分析一下。

a(); // 1
var a = 'hello world'; //2
console.log(a); // 3
function a() { // 4
    console.log('inner a function 1')
}

var a = 'hello, tomorrow'; // 5

console.log(a); // 6
function a() { // 7
    console.log('inner a function 2')
}
a(); // 8

6.png
编译阶段:

  • 1,函数调用不做处理
  • 2, 有var声明变量,在执行环境中变量环境对象上创建a变量并赋值为undefined.
Variable Environent:
    a -> undefined;
  • 3, 函数调用,不做处理
  • 4, JS引擎发现有同过function 定义的函数a,把函数定义存储到堆中,并且在变量环境对象中查找是否有a属性,有a,然后把a的值指向该函数的在堆中的位置。此时变量环境对象变成(类似这样):
Variable Environent:
    a -> function () { console.log ('inner a function 1')};
  • 5, 有var声明变量,在执行环境中变量环境对象上查找是否存在a属性,发现存在并y有值,不做处理。
  • 6, 不做处理
  • 7, JS引擎发现有同过function 定义的函数a,把函数定义存储到堆中,并且在变量环境对象中查找是否有a属性,有a,然后把a的值指向该函数的在堆中的位置。此时变量环境对象变成(类似这样):
Variable Environent:
    a -> function () { console.log ('inner a function 2')};

然后执行可执行代码:

a(); // 1
a = 'hello world'; //2
console.log(a); // 3
a = 'hello, tomorrow'; // 4
console.log(a); // 5
a(); // 6
  • 1,JS引擎变开始在变量环境对象中寻找该函数,由于变量环境对象中a存在该函数的引用,所以JS引擎便开始执行该函数,输出
inner a function 2
  • 2,赋值操作,把'hello world'赋值给a,此时变量环境对象变更为:
Variable Environent:
    a -> 'hello world';
  • 3,打印a的信息。
hello world
  • 4, 赋值操作,把'hello, tomorrow'赋值给a,此时变量环境对象变更为:
Variable Environent:
    a -> 'hello, tomorrow';
  • 5,函数调用,但是在变量环境对象中,a 的值为'hello, tomorrow',并没有指向函数的位置,所以报错,没毛病。

这就是整个过程啦,大家可能在在编译阶段的第5步会有疑虑,为什么没有覆盖,我专门验证了这个问题,截图如下:
7.png

扩展,有参数时怎么办
function c(a, b) {
    var a = 'hello, world';
    console.log(a, b);
}
c(1, 2)

8.png
相信大家都知道答案,但是具体流程大家清楚嘛,欢迎留言分析或者自己分析。

最后

欢迎大家关注我的微信公众号:匿名程序媛
一起挖坑、填坑
image

技术交流qq群:936183824


Rhinoceros
183 声望12 粉丝

以终为始