JS的执行机制

190 阅读6分钟

本文用于个人面试,部分知识未罗列出来,不用于系统学习。

变量提升

JS代码执行过程中,JS引擎把变量、函数的声明部分提升到代码开头的行为。变量被提升后,会给变量设置默认值undefined。究其原因,需要了解作用域。

作用域

详细参考下文《作用域和作用域链》

在 ES6 之前,ES 的作用域只有两种:全局作用域和函数作用域。

全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。

函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

变量提升的原因:

(1)变量在不被察觉的情况下被覆盖

(2)本应该销毁的变量没有被销毁

ES6解决变量提升的方法:提出了let和const来生成块级作用域。

JS代码的执行

JS的执行分为编译阶段(即我们常说的预编译阶段)和执行阶段。

编译阶段

编译阶段生成执行上下文(对应红宝书第三版的4.2章节)和可执行代码

js执行流程.png

同名变量和函数的处理原则

1:如果是同名的函数,JavaScript编译阶段会选择最后声明的那个。

2:如果变量和函数同名,那么在编译阶段,变量的声明会被忽略。

预编译初始值原则

1:let const在上下文对象的创建阶段不会被初始化,在代码执行阶段才会被赋值。 

2:var在上下文对象的创建阶段会被初始化为undefined。

3:表达式函数如果用let声明则不会被初始化,表达式函数如果用var声明则被初始化为undefined。

下面的代码是一个预编译常考的题目:

showName()  //1 预编译阶段函数优先级高
var showName = function() {console.log(2)} //执行阶段重新赋值覆盖了全局的function
function showName() {console.log(1)}
showName()  //2 

执行阶段

将执行上下文压入调用栈中,当全局代码、函数执行完毕后,将对应的执行上下文弹出栈。

关于栈溢出的问题,在某网课上看到了这样一个解决方法:

function runStack (n) { 
    if (n === 0) return 100; 
    return runStack( n- 2);
}
runStack(50000)

//改进。直接选择去异步队列,可以避免进栈。
function runStack (n) {
  if (n === 0) return 100;
  return setTimeout(function(){
      runStack( n- 2)},0);
    }
runStack(50000)

//更多的说法 改成为尾递归(需要在严格模式下生效)
function runStack (n, result=100) {
  if (n === 0) return result;
  return runStack( n- 2, result);
}
runStack(50000, 100)

执行上下文

上下文对象被创建后,会将该对象压入栈中,在程序执行的过程中,js总会从栈顶查找所需的数据,当程序执行结束,销毁上下文并出栈。

创建执行上下文的条件

JS执行全局代码(全局执行上下文)、调用一个函数(函数执行上下文)、调用eval函数

上下文的分类

一种是全局上下文对象,一种是函数上下文对象

全局上下文对象是在开始执行一段javascript代码时所创建的上下文对象,在html环境中,该上下文对象就是window对象。在node环境中为global对象。创建完上下文对象之后,该对象会入栈。全局上下文对象有且只有一个,只有当浏览器关闭时,全局上下文对象才会出栈。

函数上下文对象是在一个函数开始执行时所创建的上下文对象,创建完该对象以后,该对象同样的会入栈,当函数执行完毕,函数上下文对象出栈。每一次函数的调用都会创建新的函数上下文对象并入栈,哪怕是同一个函数的多次调用依然如此。

在秩序执行过程中所需要的数据,都会从栈顶的上下文对象中获取。

请看下面一段代码:

<Script>
//1.全局上下文对象入栈
var v=10;
console.log(v);//2.从栈顶的上下文中获取数据v
function f1(){
    var v1=1;
    console.log(v1);//4.从栈顶的上下文中获取数据v1
    f2();//5.f2函数上下文入栈
    //8.f1函数上下文出战
}
function f2(){
    var v2=2;
    console.log(v2);//6.从栈顶的上下文中获取数据v2
    //7.f2函数上下文出栈
}
f1();//3.f1函数上下文入栈
</script>

image.png

上下文结构

上下文内部由环境对象、词法环境、outer等构成。

  • 词法环境中存储所有以let、const声明的变量以及所有的函数。由于函数具备参数,所以在函数的上下文对象的词法环境对象中还存储了一个arguments对象用于存储参数数据
  • 环境对象中只存储以var声明的所有变量。

1.变量提升的变量存放在【环境对象】中。倘若JS引擎发现有引用类型对象,比如var c = function A(),会将函数定义放 到执行堆中去,然后在环境对象中创建一个A的属性,将属性值指向堆中函数的位置。

2.在【词法环境】内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。需要注意下,这里所讲的变量是指通过 let 或者 const 声明的变量。

3.执行上下文内部查找变量的过程(可以更好的理解作用域链):从词法环境栈顶向下查询,找到了就返回给JS引擎,没有找到就去环境对象中去寻找。仍没有找到就去外部作用域去查找。

//例如如果js代码如下
GlobalContext={ 
    var v1=10; 
    let v2=20; 
    function f(num){ 
        var v3=30; 
        let v4=45; 
        function f1(){ 
        } 
} 
f(10);

//对应的上下文内部如下
FunctionContext={ 
    词法环境对象:{ 
        v4:值, 
        f1:值, 
        arguments:[参数] 
        }, 
    变量环境对象:{ 
        v3:值 
     } 
}

image.png

作用域和作用域链

var a=100;
function f(){
    var a=10;
    console.log(a);
    function f1(){
        console.log(a);
    }
    f1();
};
f();

image.png