Java教程

JavaScript程序的执行过程(七)

本文主要是介绍JavaScript程序的执行过程(七),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

程序的执行过程

堆和栈

堆和栈本身是数据结构,堆(链表结构)、栈(栈结构)。

程序在运行的时候内存中按照逻辑分为了堆内存和栈内存。

  • 栈:栈结构的内存,比较小,速度快,操作系统会自动分配、回收。

    特点:先进后出。

  • 堆:堆结构的内存,比较大,速度慢。一般都是程序员使用的时候自己分配的。要让内存回收必须手动释放。

    特点:先进先出(排队)

在底层语言中是这样的,在js中我们的各种内存机制几乎都是自动的。

执行环境

JavaScript的运行环境分为两种:

  • Global,全局。JavaScript开始运行的时候默认运行的环境就是全局执行环境。
  • Function,函数。函数调用的时候就进入到了函数的执行环境。

每个运行环境我们通常称为执行上下文。

代码在执行的时候开辟出来一段栈空间用来说明代码的执行顺序,这个栈我们叫执行栈。

  1. 当JavaScript代码执行的时候将会先将全局执行环境压入到栈底,当执行函数的时候会创建函数的执行环境,并将函数执行环境压入到栈底。
  2. 当函数执行完成之后,函数执行环境将会从执行栈中弹出并且销毁(只要执行环境销毁和他相关的一系列的东西都会被销毁)。
  3. 一直到所有的代码都执行完成之后才将全局执行环境销毁。

执行环境的阶段

  • 创建阶段(用来做准备工作)
    • 全局执行环境:一上来就进入到了创建阶段。
    • 函数执行环境:当函数被调用的时候,但是在代码真正执行之前。
  • 执行阶段(真正的执行代码)

作用域和作用域链

  • 什么是作用域?

    作用域:就是变量起作用的范围。

  • 作用域的作用?

    隔离变量。函数外部定义的变量和函数内部定义的变量没有关系。

  • 作用域确定的时机?

    作用域是在进入到全局执行环境或函数执行环境时就确定好的。

  • 作用域种类?

    ES5里面作用域分为两种:全局作用域和局部作用域(函数作用域)

    ES6里面加入了块级作用域。

    当你一执行JavaScript就进入了全局执行环境就有了全局作用域,当执行某个函数时候就进入了函数执行环境就有了某个函数的作用域。

    如果执行环境销毁其作用域也会被销毁。

    __同一个函数调用多次将会产生不同的执行环境,每次调用都会产生一个。__因为每一次执行的环境分配的地址空间都不相同。

  • 全局变量、局部变量

    • 全局变量:定义在全局执行环境中的变量。(函数外部的变量,全局作用域)

    • 局部变量:定义在局部环境中的变量。(函数内部,函数作用域)

    • 局部变量:只能在自己的作用域中起作用,只能在函数内部使用,外部是用不到的。

      function test(){
          var a = 1;
          var b = 2;
          console.log(a);
      }
      test();
      console.log(a);
      
    • 全局变量:在整个程序的所有地方都可以使用(包括函数内部)。

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

      尽量少定义全局变量,把变量的权限最小化。

  • 作用域链

    函数内部是可以嵌套函数的。

    var a = 0;
    function test() {
        var a = 1;
        function test1() {
            console.log(a);//1
        }
        test1();
        console.log('test');//test
    }
    test();
    

    上面的代码输出1,test为什么是1是由于作用域链导致的。

    作用域是变量起作用的范围,作用域链用来描述查找变量的一系列的过程。

    一个函数在定义的时候会生成一个属性[[scope]]属性(带[[]]的都是不能访问),这个属性中存储了定义函数时作用域层级。

    在这里插入图片描述

    作用域链执行的时候才有:作用域链=当前的函数作用域+[[scope]]

    作用域是执行的时候有的;

    [[scope]]定义的时候有的;

    作用域链执行的时候才会最终生成。作用域链=当前的函数作用域+[[scope]]

    函数当前的作用域是作用域链的顶端,全局作用域是作用域链的最后端。

    在这里插入图片描述

    执行环境被销毁,作用域链也会被销毁。

    var定义变量就是将作用域限定在最近的函数作用域上。如果不是在函数中定义的变量在全局中定义这个变量,属于全局作用域。

  • 局部作用域中使用变量的查找过程:

    1. 局部作用域中是否有同名的形参,如果有就使用。

      <script>
          function test(a){
              console.log(a);
          }
          test(1);
      </script>
      
    2. 局部作用域中是否有同名的函数(函数内部也可以定义函数),如果有就使用。并且在定义之前或之后使用都可以。

      function test(){
          /*
          function a(){}
          a();
          a();
          */
          a();
          function a(){
              console.log('a');
          }
          a();
      }
      test();
      
    3. 局部作用域中是否有同名的变量声明,如果有就使用(在声明之前使用将会是undefined,声明之后使用将是值)

      function test() {
          /*
          var a;
          console.log(a);
          a = 1;
          console.log(a);
          */
          console.log(a);//undefined
          var a = 1;
          console.log(a);//1
      }
      test();
      
    4. 如果同时使用了同名的形参、变量:变量会覆盖形参,在形参下马上使用将会是实参的值,在变量定义下使用将会是变量的值。

      <script>
          function test(a){//a=2
         	 	/*
         	 	var a ;//忽略  重复声明
         	 	console.log(a);//2
         	 	a = 1;
         	 	console.log(a);//1
          	*/
              console.log(a);//2
              var a = 1;
              console.log(a);//1
          }
          test(2);
      </script>
      
    5. 同时定义了同名的形参、函数:函数会覆盖形参,在形参下马上使用或在函数定义下马上使用都是函数。

      function test(a) {
      
          console.log(a);//函数体
      
          function a() {
              console.log('a');
          }
      
          console.log(a);//函数体
      
      }
      test(2);
      
    6. 如果同时定义了同名的变量、函数:函数第一行使用将会是子函数,变量定义下使用将会是变量的值。

      function test() {
          console.log(a);//函数
          var a = 1;
          console.log(a);//1
          function a() {//子函数
              console.log('a');
          }
          console.log(a);//1
      }
      test();
      
    7. 如果同时定义了同名的形参、函数、变量声明:子函数覆盖形参、函数第一行使用时将会是子函数、变量定义下使用将会是变量值。

      function test(a) {
          console.log(a);//函数
          var a = 1;
          console.log(a);//1
          function a() {
              console.log('a');
          }
          console.log(a);//1
      }
      test(2);
      
    8. 如果局部作用域中没有该变量,则沿着作用域链向上层开始查找。

      1. 如果上层能够找到就使用了。
      2. 如果没有找到就继续向上层找,直到找到全局作用域中,如果有就使用,如果没有就报错。
  • 在局部作用域中设置变量的过程:

    1. 看函数中是否定义了同名的变量,如果定义了则直接修改本作用域中的。

      <script>
          function test(){
              var a = 1;
              console.log(a);//1
              a = 2;
              console.log(a);//2
          }
          test();
      </script>
      
    2. 如果本作用域中没有同名的变量,则沿着作用域链向上层查找,如果找到设置的是上层作用域的。

      function test1(){
          var a = 'xyz';//1 2
          function test(){
              console.log(a);//xyz
              a = 1;
              console.log(a);//1
              a = 2;
              console.log(a);//2
          }
          test();
          console.log(a);//2
      }
      test1();
      
    3. 如果到最后都没有找到则看全局作用域中是否有,如果有就修改,如果没有就声明。

      var a = '000';
      function test1(){
          function test(){
              console.log(a+'!');//000!
              a = 1;
              console.log(a+'@');//1@
              a = 2;
              console.log(a+'#');//2#
          }
          test();
          console.log(a+'$');//2$
      }
      test1();
      console.log(a + '%');//2%
      

      如:

          function test1() {
              function test() {
                  a = 1;
                  console.log(a + '@');//1@
                  a = 2;
                  console.log(a + '#');//2#
              }
              test();
              console.log(a + '$');//2$
          }
          test1();
          console.log(a + '%');//2%
      

      小总结:作用域链函数执行的时候才会生成,因为作用域链等于:当前作用域+[[scope]]

      当前的作用域:函数调用的时候生成。

      [[scope]]属性:函数定义的时候就有了,函数即使不调用也有。

      <script>
          var num = 10;
          function fun() {//[[scope]]->全局作用域
              var num = 20;
              fun2();//fun2的作用域+全局作用域
          }
          function fun2() {//[[scope]]->全局作用域
              console.log(num);
          }
          fun();//fun的作用域+全局作用域
      </script>
      

      fun的局部环境中可以调用全局中的fun2,fun2执行的时候找num,这个时候fun2的作用域链中只包含fun2的执行环境+全局执行环境

    内存中的数据存储

    • 分析1:

      <script>
          var a = 100;
          console.log(a);
      </script>
      

      进入到全局执行环境,全局执行环境发现有变量声明,在栈空间中用一段空间来存储a=100,当你执行到console.log(a);的时候从栈内存中查找a这个标识符然后找到它的值。

    在这里插入图片描述

    变量如果是基本数据类型(String、Number、Boolean、undefined、null)会在栈内存开辟空间存储标识符,然后将基本类型的值也存储在栈内存中并且和标识符对应起来。

    • 分析2:

    在这里插入图片描述

    • 分析3:

      引用类型的值存储在堆内存中,存储引用类型的时候会给堆内存一个内存地址,但是标识符还是在栈内存中,为了能够让标识符和真正的数据结合起来在栈中存储的值是引用类型的地址。

      内存地址都是十六进制0x开头。

    在这里插入图片描述

    变量如果是引用类型,会在栈内存开辟一段空间存储标识符,引用类型的值存储在堆内存中,堆内存中对应内存地址,这个内存地址会被存储到栈内存中和标识符对应起来。

    查找变量和执行环境、作用域、作用域链有关系,和堆没有关系,因为是查找标识符,标识符存储在栈里面。

    • 分析4

    在这里插入图片描述

    引用类型赋值的时候赋的是地址而不是值。

    赋址和赋值

    • 分析5:

    在这里插入图片描述

    在这里插入图片描述

    • 分析6:

      在这里插入图片描述

    • 分析7:

      在这里插入图片描述

    程序开始~结束执行了什么:

    1. 程序一开始执行,碰见了全局执行环境,创建全局执行环境压入到执行栈,全局代码执行的时候依赖的是全局环境中的东西。

      全局变量:如果是基本数据类型,值存储在栈内存,如果是引用类型这些数据存储在堆内存里面,标识符存储在栈内存中。然后要和堆内存中的地址要对应起来。

    2. 当程序遇到了函数调用,就有自己的执行环境了,然后将自己的执行环境压入到执行栈。

      局部变量:局部变量的数据存储和全局变量的存储套路是一样的只不过执行环境不一样。

      只有函数执行,局部变量才会出现。函数执行完成之后,函数环境要销毁,局部变量也就不存在了。

    3. 当函数调用完成之后,要继续执行全局里面的代码,一直到所有代码都执行完成,代表程序结束了,程序结束,全局环境最后出栈(销毁)

变量提升(预解析)

执行环境:

  • 创建阶段
  • 执行阶段

创建阶段:将带var的变量的声明以及函数的声明放在当前作用域的顶部,执行阶段:开始在原来的代码的本身应该在位置开始进行赋值和处理其他逻辑。

console.log(a);
var a = 1;
但是实际执行的不是这样,实际执行的是

var a;//在创建阶段声明的 undefined
console.log(a);//执行阶段 这时候要用到a,但是上面的a还没有赋值所以打印出来值是undefined
a=1//执行阶段时候执行  执行重新赋值


test();
function test(){
    console.log('!');
}
test();
实际执行的时候:
function test(){
    console.log('!');
}
test();
test();

注意:

  1. 不管是全局执行环境还是函数执行环境每个执行环境都会对变量、函数进行提升操作。

  2. 只有function声明的函数将会被提升函数表达式和函数构造器声明的函数不会被提升。

    <script>
        /*
        var foo;//undefined
        foo();//foo不是函数。    如果foo这个变量真的不存在的话那么就是foo not defined
        foo = function(){
            
        }
        */
        foo();
        var foo = function (){
            console.log('!!!');
        }
    
    
    </script>
    
  3. 函数声明和变量都会被提升,但是函数会被首先提升,然后才是变量(在JS中函数是一等公民)

        /*
        function foo(){//定义了一个函数foo
            console.log(1);
        }
        //重复定义foo将会被忽略。
        var foo;
        foo();
        foo = function(){
            console.log(2);
        }
        */
    
        foo();
        var foo;
        function foo(){
            console.log(1);
        }
        foo = function(){
            console.log(2);
        }
    

解析过程中foo()函数会被提升到最前面,然后是var foo但是因为函数已经被声明了所以var foo属于重复定义将会被忽略,然后执行foo()函数所以输出1,然后又定义了函数表达式foo = function()所以覆盖了前面的foo函数。

函数同名,提升过程中,后面的覆盖前面的。

小练习

  • 第一个坑

    <script>
        alert(a);
        a = 0;
    </script>
    

    报错,不是正常的变量定义,不会出现提升操作。

  • 第二个坑

    /*
    var a ;
    alert(a);//undefined
    a = 0; 
    alert(a);//0
    */
    alert(a);//undefined
    var a = 0;
    alert(a);//0
    
  • 第三坑

    <script>
        /*
        function a(){}
        var a;
        alert(a);
        a = '我是变量';
        alert(a);
        */
        alert(a);
        var a = '我是变量';
        function a() { alert('我是函数') }
        alert(a);
    </script>
    
  • 第四坑

    <script>
        /*
        function a(){}
        var a;
        alert(a);//函数
        a++;//NaN
        alert(a);//NaN
        a='我是变量';
        alert(a);//我是变量
        */
        alert(a);
        a++;//函数转换数值是NaN。
        alert(a);
        var a = '我是变量';
        function a() { alert('我是函数') }
        alert(a);
    </script>
    
  • 第五坑

    在这里插入图片描述

  • 第六坑

    在这里插入图片描述

  • 第七坑

    在这里插入图片描述

  • 第八坑

    在这里插入图片描述

这篇关于JavaScript程序的执行过程(七)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!