JS代码执行过程详解(面试中的加分项)

810 阅读8分钟

JS代码在执行过程中都干了哪些事

1. 开胃题

let a = 12;
let b = a;
b = 13;
console.log(a);
-----------------
let a = {n12};
let b = a;
b['n'] = 13;
console.log(a.n);
-----------------
let a = {n12};
let b = a;
b = {n13};
console.log(a.n);

当你看到上面这三个题的时候,可能会立马说出输出的结果,但是对于它的执行过程中开辟那些内存,以及其中值发生的变化真的清楚吗?下面以这三个题来作为今天的开场白,咱们一起来聊聊JS代码在执行过程中的一系列过程:

2.JS代码执行中遇到的几个‘词’

执行环境栈内存ECStack: 浏览器加载页面的时候,想让代码执行,首先会形成一个栈内存(执行环境栈);然后开始让代码准备执行;

堆内存Heap :存储在执行过程中引用数据类型的值:对象存储的是键值对;函数存储的是字符串

全局执行上下文EC(G): 最开始要执行的一定是全局下的代码,此时会形成一个全局代码的执行环境(全局上下文EC(G)),把EC(G)压缩到栈内存中去执行(进栈操作);每一个函数的执行也是这样的操作;

有些上下文在代码执行完成后,会从栈内存中移除去(出栈操作),但是有些情况是不能移除去的(例如:全局上下文就不能移出去);

在下一次有新的执行上下文进栈的时候,会把之前没有移出去的都放栈内存的底部,让最新要执行的在顶部执行;

GO全局对象=>window: 在全局下使用var、function声明的变量会在全局对象window中也存储一份,并且建立映射机制(一边的值改变另一边值也改变);

全局变量存储值对象VO(G): 把在全局下声明的变量存储在VO(G)中;

私有变量存储值对象AO(XX): 当形成私有作用域时,私有变量对象存储在AO(XX)中

3. 结合图片理解开胃题中的执行过程

3.1 基本数据类型值和引用数据类型值的区别

JS中的数据类型目前有两大类:基本数据类型引用数据类型,他们有什么区别呢?

基本数据类型包括number、string、Boolean、null、undefined、symbol、bigint......;他们都是把值存储在栈内存中的,操作的是值;

引用数据类型包括对象(普通对象、数组对象、正则对象、函数对象、日期对象......) 和 函数;他们都是把值存储在堆内存中,然后把堆内存的地址存储在栈内存中,后期操作的是都是堆内存的地址;

3.2 '='

JS中的'='不像数学函数中的'='那样,它是把右侧的结果(如果是一个表达式或者自执行函数等都是先计算它的结果) 赋值 给左侧的变量

例如:let a = 1; 就是定义了一个变量a,并且把右侧的1赋值给左侧的变量a

例如: let b = 1+2; 定义了一个变量b,把右侧的运算结果3赋值给左侧的变量b

3.3 变量的关联

在JS中,一个变量只能关联一个值,但是一个值却可以关联多个变量

3.4 执行过程

let a = 12; 它的执行过程是:先创建一个值是12,放在栈内存中;然后再创建一个变量a;最后把两个关联起来;

3.5 开胃题讲解

结合上面的知识来分析一下开胃题

  • 首先浏览器会先开辟一个供代码执行的栈内存
  • 在栈内存中开辟一个全局代码执行的环境EC(G),让代码在这个作用域中一步步的执行
let a = 12;
let b = a;
b = 13;
console.log(a);
-----------------
let a = {n12};
let b = a;
b['n'] = 13;//先创建值13存储在栈内存中,然后让b执行的堆内存中的n这个属性改为13
console.log(a.n);
-----------------
let a = {n12};
let b = a;
b = {n13}; //先创建一个堆内存用来存储值,然后把堆内存的地址存储在栈内存中和变量关联,因为b这个变量只能指向一个值,因此此时b的指向就改为`n:13`的这个堆内存
console.log(a.n);

4. 变量提升

变量提升是在当前执行上下文中(不管是全局的还是函数执行私有的),JS代码自上而下执行之前,都是先把带varfunction 的进行变量提升;而letconst等没有变量提升机制;

var:会进行提前声明,并且会把声明的变量存储在GO全局变量对象中,之后会存在映射机制;

function :会进行提前声明+定义,但是如果function出现在if/for等这些大括号内只会提前声明,并不会定义;

5. var let function const 这四者的区别

  1. varfunction会进行变量提升,而letconst没有变量提升机制;

  2. varfunction声明的变量在定义之前可以使用,而letconst声明的变量在声明之前不能使用;

  3. 在全局执行上下文中使用varfunction声明的变量会向window中也映射一份,而letconst不会;

  4. typeof检测数据类型的时候有一个‘暂时性死区’的问题:如果没有定义这个变量 返回结果是undefined,而不是报错;但是在使用letconst声明这个变量之前使用typeof检测会报错;

  5. if/for这样的大括号内,如果有使用let或者const定义的变量,那么它就是私有的,会存放在if/for这个块作用域中;

  6. 使用const定义的某些值是可以修改的。当const定义基本数据类型的时候不可以修改;但是const定义的是引用数据类型,那么就可以修改它执行的这个堆内存中的内容,此时并没有修改const这个变量的指向。
const a = 10;  //这里a是不可以修改,如果修改会报错
const obj = {age:21};
obj = [10,20];     //这是不可以修改的,如果修改会改变他的指向,因此会报错
obj.age = 22;      //这是可以修改的,他修改的是obj指向的堆内存中的值,并没有改变obj的指向

6. 作用域 与 作用域链

6.1 作用域链的形成

在某一个上下文中创建函数,除了开辟堆内存和赋值之外,还多做了一件事情‘给当前函数设置作用域链[[scope]] = 当前函数创建时候所在的上下文’

6.2 作用域链的查找机制

在当前上下文中,代码执行过程中遇到一个变量,首先看它是否为私有的

  • 如果是私有的,接下来的所有操作,都是操作自己的,和别人没有关系
  • 如果不是私有的,则按照scopeChain作用域链进行操作,在哪个上下文这个找到,当前变量就是谁的...一直找到全局上下文为止
    • 如果找到EC(G)都找不到:如果是获取变量值就会报错,如果是设置值,相当于给GO加属性

7. 函数的执行过程

创建函数

  • 在变量提升过程中,如果有带function的,直接定义加创建(自执行函数和函数表达式以及if/for内的除外)
  • 形成自己的作用域:[[scope]] = EC(XX);

执行函数

  • 开辟一个私有的上下文
  • 进栈执行
    • 初始化作用域链 [[scope-chain]] = <当前上下文,上级上下文>
    • 初始化this
    • 初始化arguments = {0:1,1:2,length:2}
    • 形参赋值,分别给每个形参赋值
    • 变量提升:var 和 function
    • 代码执行
    • 是否销毁上下文 :如果外面有占用则不销毁,如果没有执行完就立即销毁

8. 检测成果

下面这个题就是结合了变量提升、函数执行等的一道题目,我把解题的思路使用画图的方式展示出来了,如果还是有不明白的地方,欢迎在下方的讨论去交流

var x=5,
    y=6;
function func(){
    x+=y;
    func=function(y){
      console.log(y + (--x));
    };
    console.log(x, y);
}
func(4);
func(3);
console.log(x, y);

9. 总结

上面关于JS中的变量提升是JS语言中需要攻克的四座大山之一,是我们之后在写程序时候快速解决BUG的基础知识之一。如果在面试过程中,可以把代码每一步的执行过程都说出来,那也是一个加分项。加油