01-EC 与 ScopeChain

6/22/2022

# 1. 什么是预解析

代码段是指一个script标签就是一个代码段。JS代码在执行时,是一个代码段一个代码段执行。

<!-- 代码段是彼此独立的,上面的代码段报错了,不会影响下面的代码段 -->
<!-- 1个script标签就是一个代码段 -->
<script>
    // 在一个代码段中就可以写JS代码
    var a = 110;

    // 上面代码段不能使用下面的代码段中定义的数据
    console.log(b); // ReferenceError: b is not defined
</script>
<!-- 一个网页中可以有多个代码段 -->
<script>
    var b = 220;
    // 可以在下面的代码段中使用上面的代码段中的数据
    console.log(a);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

JS代码的执行分两个阶段:一个叫预解析,一个叫执行,预解析结束后,才会进入到执行阶段。

什么是预解析?

  • 浏览器在执行JS代码的时候会分成两部分操作:预解析以及逐行执行代码
  • 也就是说浏览器不会直接执行代码, 而是加工处理之后再执行,
  • 这个加工处理的过程, 我们就称之为预解析

预编译期间做了什么?

  • 把声明提升:加var的变量就是被提升,function声明的函数也会提升,提升到代码段的最前面。
  • 函数内部的局部变量,提升到函数体的最前面。
  • 注意:变量的提升仅仅是声明 函数的提升不只提升了声明,也提升赋值

练习题:

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

    fn();
    function fn(){
        console.log("我是一个fn函数~");
    }
</script>
<script>
    // --------- 分析 
    // 下面的代码中有两个声明  加var的变量a    function声明的函数 fn
    // 两者都要提升  变量仅仅提升声明  function提升的整体
    console.log(a);   // und
    var a = 110;
    console.log(a);  // 110

    fn();  // 也可以执行
    function fn(){
        console.log("我是一个fn函数~");
    }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
    // --------- 原题
    g();
    var g = function(){
        console.log("g函数执行了....");
    };
</script>

<script>
    // --------- 分析 
    // g是一个变量  只不这个变量的值是一个函数  函数是一种数据类型
    // 进行预编译时,仅仅是提升了var g;  
    // var g; g的值是und    und();  报错了  
    g();  // TypeError: g is not a function  只有函数才可以加()调用
    var g = function(){
        console.log("g函数执行了....");
    }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
    // --------- 原题
    console.log(i);
    for(var i=0; i<10; i++){ }
    console.log(i);
</script>

<script>
    // --------- 分析 
    // i是全局变量  只有定义在函数内部的变量才是局部变量
    // var i会提升,所以前面输出的i是und
    console.log(i);  // und
    for(var i=0; i<10; i++){ } // for循环对i进行了赋值
    console.log(i);  // 10  
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
    // --------- 原题
    var a = 666;
    fn()
    function fn(){
        var b = 777;
        console.log(a);
        console.log(b);
        console.log(c);
        var a = 888;
        var c = 999;
    }
</script>

<script>
    // --------- 分析 
    // 提升: 
    //    全局的:var a;   fn整体;  提升到代码段最前面 
    //    fn内部中的var b; var a; var c;都提升;  提升到函数体的最前面
    // 
    var a = 666;
    fn()
    // fn产生一个局部作用域,它的父级作用域是全局作用域
    function fn(){
        // var b; var a; var c;都提升了
        var b = 777;
        // 找a  自己有  使用自己的  如果自己没有  使用父级作用域的数据
        console.log(a);  // und 自己函数体内如果有数据,就使用自己的数据
        console.log(b);  // 777
        console.log(c);  // und
        var a = 888;
        var c = 999;
    }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script>
    // --------- 原题
    console.log(value);
    var value = 123;
    function value(){
        console.log("我是value函数...");
    }
    console.log(value);
</script>

<script>
    // --------- 分析 
    // 预编译:
    //     提升:
    //        var value;会提升   value的值是und
    //        value函数整体也会提升   
    //             如果变量名和函数名一样,变量提升了,函数也提升了,提升后只会存在一个名字
    //             问题是value的值是函数,也就意味着,变量的value就被覆盖了
    console.log(value);  // value是函数
    var value = 123;  // 123赋值给了value,value又变成了123
    function value(){
        console.log("我是value函数...");
    }
    console.log(value); // 123
    // 提升的同名变量名和函数名,函数会覆盖变量,函数在JS中是一等公民,优先级高
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
    // --------- 原题
    console.log(a);
    a = 666;
    console.log(a);
</script>

<script>
    // --------- 分析 
    //   没有加var的变量不会提升
    // 
    console.log(a);  // a is not defined   /   und 
    a = 666;
    console.log(a);  // 在一个代码段中,上面的代码出错了,下面的代码不会执行了
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
    // --------- 原题
    function fn(){
        console.log(a);
        a = 666;
        console.log(a);
    }
    fn();
    console.log(a);
</script>

<script>
    // --------- 分析 
    // 提升:fn整体
    //    
    function fn(){
        console.log(a);  // 报错   找a  没a  a is not defined  
        a = 666;
        console.log(a);  // 不执行  
    }
    fn();
    console.log(a);  // 不执行  
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
    // --------- 原题
    function fn(){
        a = 110;
    }
    console.log(a);
</script>

<script>
    // --------- 分析 
    // 函数没有调用,相当于函数没有写,没有执行
    // 函数体中定义的所有的数据,都不会执行
    function fn(){
        a = 110;  // 没有加var的变量是全局变量
    }
    // fn();  // 如果调用了,结果是110
    console.log(a);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
    // --------- 原题
    gn()
    function gn(){
        var k = 123;
        console.log(k);
    }
    console.log(k);
</script>

<script>
    // --------- 分析 
    // gn整体要提升
    gn()
    function gn(){
        var k = 123;  // var k要提升到函数体最前面
        console.log(k); // 123
    }
    console.log(k);  // 报错
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 2. 执行上下文和作用域链

内存分区:

  • 我们只需要掌握两个区,一个是栈区,一个是堆区。
  • 基本数据类型,是存储在栈区的,引用数据类型是存储在堆区,堆区的地址,还是保存在栈区

# 2.1 全局代码和函数代码中产生的EC

JS代码分两类:

  • 全局代码:函数外面的代码都是全局代码
  • 函数代码:一个函数就是一个局部代码
<script>
    // 函数外面的就是全局代码
    var a = 1;
    var b = 2;
    function fn(){
        // 函数里面的是局部代码
        var c = 3;
        var d = 4;
    }
    fn()
    fn()
</script>
1
2
3
4
5
6
7
8
9
10
11
12

分析如下:

  • 全局代码执行产生ECG(Execution Context Gloable),每当调用一个函数,就产生一个函数的EC。每产生一个EC,需要放到ECS(Execution Context Stack),当函数调用完毕,这个EC就是出栈,出栈后的EC,会被销毁,所谓的销毁指是它们分配的内存空间都要被释放掉。

总结:

  • 当全局代码执行时,就会产生一个全局的执行上下文,EC(G);
  • 当函数代码执行时,就会产生一个局部的执行上下文,EC(Fn)。只要调用一个函数,就会产生一个局部执行上下文。调用100个函数,就会产生100个执行上下文。

执行上下文栈:

  • js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。
  • Execute Context Stack ===> ECS

代码执行流程如下:

  • JS在执行代码时,肯定先执行全局代码,就会产生EC(G),这个EC(G)就要入栈。当我们调用一个函数,就会产生一个局部的执行上下文,此时这个局部的执行上下文也要入栈。当函数调用完毕后,这个EC就要出栈,又进入EC(G),当全局代码执行完后,EC(G)也要出栈。

执行上下文的作用:

  • 提供数据,全局代码,肯定需要去全局的执行上下文中找数据。

# 2.2 全局代码和函数代码中产生的EC

js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)

  • 该对象 所有的作用域(scope)都可以访问;
  • 里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
  • 其中还有一个window属性指向自己;说白了,GO就是window
  • 只要我们写的全局变量或在全局中写的函数,都会挂载到window上面

代码如下:

<script>
    console.log(window); // GO

    var a = 110;
    var b = 666;
    var res = a+b;
    function fn(){
        console.log("fn...");
    }

    console.log(window.a);
    console.log(window.b);
    window.fn()

    // 代码开始执行之前,JS引擎会帮我们创建一个全局对象,叫GO
    // 换句话,说到window,指的就是GO
    // GO中还需要放我们创建出来的全局变量和全局函数
    var globalObject = {
        String:"类",
        Data:"类",
        setTimeout:"函数",
        alert:"函数",
        // ....
        window:globalObject, // GO中有一个特殊的属性,叫window  window还是指向GO
        a:und,
        b:und,
        res:und,
        fn:"函数"
    };
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script>
    var n = 110;
    console.log(n);  // 110
    console.log(window.n)  // 110

    m = 220;
    console.log(m);  // 220
    console.log(window.m);  // 220 m可以放到GO中,因为是全局变量

    console.log(window.name);  // 人家给GO中放了一个name, 现在是空串
    console.log(window.x);  // 访问对象中不存在的属性,结果是Und
</script>
1
2
3
4
5
6
7
8
9
10
11
12

ECG被放入到ECS中里面包含两部分内容:

  • 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值;这个过程也称之为变量的作用域提升(hoisting)
  • 第二部分:在代码执行中,对变量赋值,或者执行其他的函数;

遇到函数如何执行?

  • 在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且压入到EC Stack中。
<script>
    var a = 1;
    var b = 2;
    var res = a+b;
    console.log(res);

    function fn(){
        console.log("我是fn函数....");
    }
    // 当调用函数时,就会产生一个EC(fn)
    fn();
</script>
1
2
3
4
5
6
7
8
9
10
11
12

ECF中包含三部分内容:

  • 第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO): AO中包含形参、arguments、函数定义和指向函数对象、定义的变量;
  • 第二部分:作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
  • 第三部分:this绑定的值:这个我们后续会详细解析;

练习题(面试题)

<script>
    var n = 110;
    console.log(n);
    console.log(window.n);
    m = 220;
    console.log(m);
    console.log(x);    
</script>
1
2
3
4
5
6
7
8
<script>
    var a = 1;
    var b = "hello";
    function fn(){
        console.log("fn...");
    }
    var arr = ["a","b","c"];
    var obj = {name:"wc",age:100}
</script>
1
2
3
4
5
6
7
8
9
<script>
    function fn(a){
        console.log(a);
    }
    fn(110)
    console.log(a);
</script>
1
2
3
4
5
6
7
<script>
    var arr = [11, 22];
    function fn(arr) {
        arr[0] = 100;
        arr = [666];
        arr[0] = 0;
        console.log(arr);   
    }
    fn(arr);
    console.log(arr);
</script>
1
2
3
4
5
6
7
8
9
10
11
<script>
    var a = 1;
    var b = 1;
    function gn(){
        console.log(a,b);
        var a = b = 2;
        console.log(a,b);
    }
    gn();
    console.log(a,b);
</script>
1
2
3
4
5
6
7
8
9
10
11
<script>
    var a = 1;
    var obj = { uname:"wangcai" }
    function fn(){
        var a2 = a;
        obj2 = obj;
        a2 = a;
        obj.uname = "xiaoqiang";
        console.log(a2);
        console.log(obj2);
    }
    fn();
    console.log(a);
    console.log(obj);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
    // 同名的变量名,只会提升第1个
    var a = 1;
    var a = 2;
    var a = 3; 
    console.log(a);

    // 预编译后的代码如下:
    var a;
    a = 1;
    a = 2;
    a = 3;
    console.log(a);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
    // 提升:var a;  
    // a函数整体提升   a = funciton(){};
    // 执行:a = 1;
    var a = 1;
    function a(){
        console.log("a...");
    }
    console.log(a); // 函数a 
</script>
1
2
3
4
5
6
7
8
9
10
<script>
    var a = 10;
    // 赋值操作就是把栈区(GO,AO)中的数据,copy一份,赋值给变量
    var b = a;
    console.log(a,b); // 10 10
    b = 1;  // 把1重新赋值给b所对应的内存空间
    console.log(a,b); // 10  1
</script>
1
2
3
4
5
6
7
8
<script>
    var a = [1,2];
    // 把栈区的地址赋赋值给b, a和b指向同一个内存空间
    var b = a;
    console.log(a,b); // [1,2]   [1,2]
    // 把一个新堆的地址重新赋值给b  b指向一个新堆空间
    b = [3,4];
    console.log(a,b); // [1,2]  [3,4] 
</script> 
1
2
3
4
5
6
7
8
9
<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>
1
2
3
4
5
6
7
<script>
    var a = [1,2];
    var b = [1,2];
    // a 和 b 是地址  == 比较时比较的是地址 不同的堆,地址是不可能一样
    console.log(a == b);  // false
    console.log(a === b);  // false
</script>
1
2
3
4
5
6
7
<script>
    var a = 110;
    var b = 110;
    console.log(a == b); // true
</script>
1
2
3
4
5
<script>
    var a = [1,2];
    // a 和 b中保存的地址是一样的
    var b = a;
    console.log(a == b); // true
    console.log(a === b); // true
</script>
1
2
3
4
5
6
7
<script>
   console.log(a,b); // 报错:b is not defined
   var a = b = 2;
</script>
1
2
3
4
<script>
    var a = {m:666};
    var b = a;
    b = {m:888};
    console.log(a.m); // {m:666}
</script>
1
2
3
4
5
6
<script>
    var a = {n:12};
    var b = a;
    b.n = 13;
    console.log(a.n);  // 13
</script>
1
2
3
4
5
6
<script>
    console.log(a); // 报错  
    a = 111;
</script>
1
2
3
4
<script>
    var m = 1;
    n = 2;
    console.log(window.m); // 1
    console.log(window.n);  // 2
</script>
1
2
3
4
5
6
<script>
    function fn(){
        var a = 111;
    }
    fn();
    console.log(window.a); // 访问一个对象上不存在的属性,结果是und
</script>
1
2
3
4
5
6
7
<script>
    var a = -1;
    if(++a){  // ++a 整体是新值 0 => false
        console.log("666");
    }else{
        console.log("888");
    }
</script>
1
2
3
4
5
6
7
8
<script>
    console.log(a,b); // und  und 
    if(true){  
        var a = 1;
    }else{
        var b = 2;
    }
    console.log(a,b); // 1 und
</script>
1
2
3
4
5
6
7
8
9
<script>
    var obj = {
        name:"wc",
        age:18
    }
    // in是一个运算符  判断一个属性是否属性某个对象
    console.log("name" in obj); // true
    console.log("score" in obj); // false
</script>
1
2
3
4
5
6
7
8
9
<script>
    var a;
    console.log(a);  // und
    if("a" in window){
        a = 110;
    }
    console.log(a);  // 110
</script>
1
2
3
4
5
6
7
8
<script>
    console.log(a);  // und
    if("a" in window){
        var a = 110;
    }
    console.log(a);  110
</script>
1
2
3
4
5
6
7
<script>
    var a = 100;
    function fn(){
        console.log(a);
        return 
        var a = 110;
    }
    fn();
</script>
1
2
3
4
5
6
7
8
9
<script>
    var n = 100;
    function foo(){
        n = 200;
    }
    foo()
    console.log(n);
</script>
1
2
3
4
5
6
7
8
<script>
    function fn(){
        var a = b = 100;
    }
    fn();
    console.log(a);
    console.log(b);
</script>
1
2
3
4
5
6
7
8
<script>
    var n = 100;
    // 找一个函数的父的EC,看的是函数的定义处,不是调用处
    function fn(){
        console.log(n);
    }
    function gn(){
        var n = 200;
        console.log(n);
        fn()
    }
    gn() 
    console.log(n); 
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Last Updated: 12/25/2022, 10:02:14 PM