JavaScript中的数据储存(执行上下文EC、栈空间ECS、堆空间)

170 阅读6分钟

摘要

本篇学习总结简要概述JavaScript语言代码预解析与执行的过程,以及在这过程中内存的分配与释放。

本文通过浏览器对JS代码在代码段内与代码段间的执行特点,引出代码执行的过程,运用《蛋与杯子的存放和取出》的通俗而又生动的比喻,归纳总结代码从被浏览器预解析,形成执行上下文EC==>入栈==>执行==>出栈(释放内存)的主要路线,以及在执行阶段执行上下文的内数据的指向、作用域对数据的限制特点、作用域链发挥的用处。最后运用大量习题反复巩固知识点,形成系统知识体系,并引出闭包和原型链知识点的思考。

关键字:JavaScript、代码段、预解析、行上下文EC、作用域、作用域链。

一、JavaScript代码段的概念

1. 什么是代码段: 在JavaScript(下面一律称作JS)中,一对<script> </script> 标签之间就是一个代码段。浏览器对一个代码段内JS代码预解析完成后,按从上到下的顺序执行代码。

2. 代码段内部代码执行的特点: 在一个代码段内,代码出现错误,浏览器就会在控制台(Console),打印出错误类型及错误位置,并停止错误位置以下的代码执行,开始下一个代码段内代码的预解析与执行。

3. 代码段间代码执行的特点: 每一个代码段之间是彼此独立的,浏览器在执行代码时,如果上面的代码段报错,浏览器会跳到下一段代码,上一段代码不会影响下面的代码段的执行。

4. 一个页面中可以有多个代码段

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

    // 使用了没有声明的变量
    console.log(c);
    // 报错:ReferenceError  引用错误
    // 同一个代码码段如报了引用错误,这个代码段的错误位置以下的代码停止执行
    console.log(a);//不执行
</script>




<!-- 一张页面中可以有多个代码段 -->
<script>
    var b = 220;
    console.log(b);
    // 上面的代码段中定义数,在下面的代码中可以执行
    console.log(a);
</script>

控制台打印结果:

image.png

二、浏览器中JavaScript语言的执行

在代码段内,JS代码不是随意访问的,而是遵循严格的规则:全局作用域下的代码可以在代码段内被全局访问,局部作用域下的代码只能在本作用域内访问。

在浏览器中,JS代码执行过程可分解为两个阶段:预解析阶段(也称:预编译阶段)、代码执行阶段

浏览器预解析完(声明提升),马上执行代码(赋值或赋值或者赋地址),是一个连续的过程

2.1 浏览器对JS代码预解析时都做了些什么?预解析与代码执行的过程具体是怎样的?

1. 声明提升: 加var的变量 提升的是声明,没有赋值; function声明的函数要整体提升到代码段的最前面。

<script>
    // 预解析提升:
    // 相当于var a ;
    // 相当于 function fn() {console.log('我是一个函数');}
    //执行代码:
    console.log(a); //undefined   预解析并未提升变量值,所以返回undefined
    var a = 110;//执行到此步 相当于给 变量a赋值110;
    console.log(a);//110

    fn(); //'我是一个函数'
    //浏览器预解析时,函数整体都提升到了最前面
    function fn() {
        console.log('我是一个函数');
    }
</script>

控制台打印结果:

image.png

将函数体赋值给变量的情况

<script>
    // 相当于 var g = undefined;
    g(); //Uncaught TypeError: g is not a function  输入错误
    // 加var的变量仅提升声明 不提升赋值,所以此变量的值一开始是undefined
    // g 此时是值为undefined的变量   而不是一个函数function ,所以不可以用g()调用



    // 函数表达式
    // 本质是一个变量, var 用来声明变量
    // 这个变量的值是函数  函数也是一种数据
    var g = function() {
        console.log('g....');
    }
</script>

控制台打印结果:

image.png

2. 声明提升与执行的具体过程: 全局变量提升到页面window最上面, 如果在函数内部的(var声明)局部变量,就提升到函数内部的最前面

<script>
    // 1、最外层预解析提升:

    // var a ;
    // 2、函数整体提升与内部预解析:
    // function fn() {
    // var b ;
    // c = 111;
    // console.log(b);
    // console.log('我是一个函数');}


    //3、执行代码:
    console.log(a); //undefined  预解析并未提升变量值,所以返回undefined
    var a = 110; //执行到此步 相当于给 变量a赋值110
    console.log(a); //110

    fn();
    //浏览器预解析时,函数整体都提升到了最前面
    function fn() {
        console.log(b); //undefined
        var b = 220; //执行到此步 相当于给 变量b赋值220
        c = 111; //不是var 声明的全局变量  
        console.log(b);
        console.log('我是一个函数');
    }
    console.log(c); //111
</script>

控制台打印结果:

image.png 3. 特别注意: 加 var 的变量,仅仅是提升声明;函数提升的不仅只有声明,还有赋值。

三、数据与数据储存(执行的JS代码数据在内存区的分配)

3.1 数据的容器 栈(stack)和 堆(heap)

JS代码是运行在计算机(或者手机等)硬件设备上的,以计算机来说,计算机储存分为RAM内存和ROM外存(也叫硬盘),计算机的RAM内存相当于一个中转站,计算机所需调取数据是从RAM内存中调取,RAM内存的特点是数据传输速度快,而RAM内存的数据又是从ROM外存中调取的,本次重点理解RAM内存中的两个区 栈(stack)、堆(heap)

image.png

3.2 数据的分类

数据可以分为两类:原始类型(也叫:简单数据类型、基本数据类型)、对象Object(也叫:复杂数据类型)。

原始类型包括:Number(数值型)、String(字符串型)、undefined、null(空)、Boolean(布尔型)、Symbol、BigInt(超大整数,使用的时候后面必须加n,一个可以表示任意精度的新的数字原始类型)。----------uSSNnBB

对象Object包括:对象、函数、数组、内置对象:window ,document(Dom), Math数学对象,Date日期对象等。除了基础类型以外的,其它都属于复杂数据类型。

3.3 JS全局代码、局部代码、执行上下文

3.3.1 JS全局代码与局部代码

全局代码:全局作用域下的代码,默认进入<script> </script> 标签,就会执行全局代码。

局部代码:局部作用域下的代码,一个函数就是一个局部代码。

<script>
   // a、b位于全局代码中
    var a = 1;
    var b = 2;

    function fn() {
     // c d位于局部代码中
     var c = 3;
     var d = 4;
    }
    
</script> 

3.3.2 JS代码执行上下文(EC)、执行上下文栈(ECS)

全局代码执行时,就会产生全局执行上下文 ( Execution Context Global )===>ECG

每当调用一个函数,就会产生一个局部的执行上下文EC,调用100个函数,就会产生100个执行上下文EC

执行上下文的产生,都需要放到栈中,这个栈叫做 执行上下文栈(Execution Context Stack) ===>ECS

当函数调用完毕,函数的EC就要出栈,当ECG执行完毕,ECG也要出栈。

执行上下文出入栈以及堆内GO 通俗理解

1)当全局代码执行,产生ECG (第一个是鸭蛋);

2)当函数执行,产生EC(fn),每调用一次就产生一个EC(后面都是鸡蛋);

3)ECG或EC(fn)都需要放到ECS中,ECS是执行上下文栈(一个杯子),先放进去的执行上下文后

拿出来;

4)当函数调用完毕,就出栈(内存释放)。(所谓出栈就是把蛋从杯子中拿出来);

 JS在执行代码时,肯定先执行全局代码,就会产生EC(G),这个EC(G)就要入栈。当我们调用一个函
 数,就会产生一个局部执行上下文EC(fn),此时,这个局部执行上下文EC(fn)也要入栈,当函数调
 用完毕后,这个 EC(fn)在堆内没有引用就要出栈(被删除)释放内存,调用其它函数又进入其他EC
 执行完毕后又,出以此类推,当全局代码执行完后,EC(G)也要出栈。
 
 EC 的作用:给代码提供数据,代码中需要的数据,都要从EC中找。
 
 
 


 JS代码在执行时,会在  堆内存  中创建一个去全局对象,Global Object  简称GO;在浏览器
 中,这个GO 说白了就是window,是一个全局对象,是属性的无序集合。
 BOM API就是window属性,如:Data、Array、String、Number、setTimeout,aleart...
 
 声明的全局变量和在全局代码中写的函数,都会挂在GO上
 
 

image.png

↓↓ 以下代码结合图片有助于更深入理解 ↓↓

<script>
    var a = 110;
    var b = 220;
    console.log(window.a);//110
    console.log(window.b);//220
    function fn() {
    console.log('fn.....');//'fn.....'
    //没有return
}
    console.log(window.fn());//undefined  fn()没有返回任何数据所以是undefined
</script>

控制台打印结果:

image.png

<script>
    // GO:内置的很多属性,和我们自己定义的全局变量和全局函数
    // EC:EC给代码提供数据
    var n = 110;
    console.log(n); //110   找n,去ECG中找  ECG中有 VO说白了就是GO
    console.log(window.n); //110  直接去GO中找,有,找到
    var m = 220; //
    console.log(m); //220   找m  去ECG中找   GO中有m  找到了220
    // console.log(x); // 找x 去ECG中找,找不到,报错,x is not defined  报错不执行该行代码段报错开始以下代码
    console.log(name);// (为空字符串  “”)    name是Object内置属性之一 
     console.dir(window); //name是Object内置属性之一
</script>

控制台打印结果:

image.png

image.png

<script>
    function fn(a) {
        console.log(a); //100
    }
    fn(100);
    console.log(a); //报错  引用错误 a is not defined
</script>

image.png

控制台打印结果:

image.png

↓↓ 执行上下文练习题 ↓↓

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

<script>
    // 加var 的同名变量智慧提升第一个  后面的相当于赋值
    var a = 1;
    var a = 2;
    var a = 3;
    console.log(a);
    // 经浏览器预解析 提升后
    var a;
    a = 1;
    a = 2;
    a = 3;
    console.log(a); //3
</script>
<script>
    var a = 1;

    function a() {
        console.log('a...');
    }
    console.log(a); //1

    // 分析
    // 浏览器预编译后:
    // var a; //=und  函数整体提升,a变成函数a()   

    // 编译后执行:
    // 对a执行赋值1
    // a = 1;
</script>

↓此段代码解释函数声明优先级高于变量声明 ↓

<script>
    // 在JS中函数的优先级高
    function a() {
        console.log('a...');
    }
    var a;
    console.log(a); //function a() { console.log('a...')}

</script>
<script>
    var a = 10;
    var b = a;
    // ab 各占一个内存空间
    console.log(a, b); //10  10 
    b = 1;
    console.log(a, b); //10  1
</script>
<script>
    var a = [1, 2];
    var b = a;
    console.log(a, b); //[1, 2]  [1, 2]
    b = [3, 4];
    console.log(a, b); //[1, 2]  [3, 4]
</script>
<script>
    var a = [1, 2];
    var b = a;
    console.log(a, b); // [1, 2]  [1, 2]
    b[0] =110;
    console.log(a, b); // [110,2]  [110,2]
</script>
<script>
    var a = [1, 2];
    var b = [1, 2];
    //ab 栈区两个的地址不一样
    console.log(a==b); // false
    console.log(a===b); // false
</script>
<script>
    var a = 110;
    var b = 110;
    //ab 栈区中的数值是一样的
    console.log(a==b); // true
</script>
<script>
    var a = [1, 2];
    var b = a;
    //ab  两个栈区的地址都一样
    console.log(a == b); // true
    console.log(a === b); // true
</script>
<script>
    console.log(a, b); //a是und b是报错ReferenceErro引用错误  ,报错的最小单位是行  所以整体是报错
    var a = b = 2;
</script>
<script>
    var a = {
        m: 666
    }
    var b = a; //把a地址赋值给b
    b = { //将b的值指向新的堆
        m: 888
    }
    console.log(a.m); //666
    console.log(b.m); //888
</script>
<script>
    var a = {
        n: 12
    }
    var b = a;
    b.n = 13; //通过ba的栈区的地址相同,修改了堆区的共同指向的引用对象
    console.log(a.n); //13
</script>
<script>
    console.log(a); //报错  Uncaught ReferenceError: a is not defined...
    a = 111;
</script>
<script>
    var m = 1;
    n = 2;
    //m n都是 全局变量,都放在栈区 的ECG 的GO
    console.log(window.m); //1
    console.log(window.n); //2
</script>
<script>
    function fn() {
         var a = 111;
    }
    fn();
    // a.b 找a对象 走的是  作用域链
    // a.b 找b属性 走的是  原型链
    console.log(a); //RefrenceErro:a is not defined
    console.log(window.a); //und
</script>
<script>
    var a = -1;
    if (++a) {
        // 执行一次++a ==> a的值是0 ,++a整体的值是0;++a整体用的是新值

        // 执行一次a++ ==> a的值是0 ,a++整体的值是-1;a++整体用的是旧值

        // 不管++在前还是在后,a都要加1
        // 此处括号内++a 值为新值0
        console.log('666');
    } else {
        console.log('888'); //888
    }
</script>
<script>
    console.log(a, b); //und und 
    // var 不管写在哪都要提升
    if (true) {
        var a = 1;
    } else {
        var b = 2;
    }
    console.log(a, b); // 1 und
</script>
<script>
    var obj = {
            name: 'wc',
            age: 18
        }
        // in 是一个运算符, 判断一个属性是否是一个对象的属性
        // 不管是私有属性,还是公有属性
    console.log('name' in obj); //true
    console.log('score' in obj); //false
</script>
<script>
    var a;
    console.log(a); //und
    if ('a' in window) {
        a = 110;
    }
    console.log(a); //110
</script>
<script>
    console.log(a); //und
    if ('a' in window) {
        var a = 110;
    }
    console.log(a); //110
</script>
<script>
    var a = 100;

    function fn() {
        console.log(a); //und
        return
        var a = 110; //预编译时已经提升到函数局部作用域最前面
    }
    fn();
</script>
<script>
    var n = 100;

    function foo() {
        n = 200;
    }
    foo();
    console.log(n); //200
</script>
<script>
    function fn() {
        var a = b = 100; //相当于 var a = 100 ;  b = 100 ;
    }
    fn();
    console.log(a); //报错 Uncaught ReferenceError: a is not defined 下面不执行
    console.log(b); //报错
</script>
<script>
    var n = 100;

    function fn() {
        console.log(n); //100
    }

    function gn() {
        var n = 200;
        console.log(n); //200
        fn();
    }
    gn();
    console.log(n); //100
<script>
    var n = 100;

    function fn() {
        console.log(n); //100  EC(fn)中无 n ,通过作用域链去父级(声明fn位置的父级)寻找
    }

    function gn() {
        var n = 200;
        console.log(n); //200
        fn();
    }
    gn();
    console.log(n); //100

    // 控制台:
    // 200
    // 100
    // 100
</script>

引用文章

js的存储方式杂记 - 掘金 (juejin.cn)

推荐文章

JavaScript中的作用域链详解

▶如有总结错误请留言 谢谢!