js高级-作用域提升-作用域链

193 阅读10分钟

什么是作用域的提升?

        javaScript的作用域的提升是指在作用域中的变量的提升和作用域中的函数的提升,js引擎在编译过程中会将代码中的变量(包
    括函数中的变量)和函数声明提前,使前端开发人员可以灵活的编写代码,不需要再遵守必须在函数和变量声明之后再使用这些
    变量和函数。
        在代码上体现为
             (1)函数的提升:一个函数声明之前可以使用这个函数。
             (2)变量的提升:函数可以跨作用域的使用其作用域链上的变量和函数。
             (3)函数中变量的提升:本质上仍旧属于变量的提升。
        简单总结,作用域的提升包含变量的提升和函数的提升两个部分。
           (1)变量的提升是指,js引擎会将变量的声明提升至它所在的作用域或函数顶部。
           (2)函数的提升是指,js引擎会将函数的声明提升至它所在的作用域的顶部,同时会对函数中的变量的声明提升到这个
     函数作用域的顶部。

怎么理解作用域的提升?

      要真正理解作用域的提升,需要要弄明白如下几个问题。
          1.js代码的执行过程
          2.什么是作用域?
          3.js都有哪几种作用域?
          4.什么是作用域链?
          5.全局作用域中的变量的提升和函数的提升
          6.局部作用域中的变量的提升和函数的提升
      

javaScript 的代码的执行流程

        js是解释性语言,这个是理解js代码运行过程的前提条件,什么是解释性语言?就是说代码的执行过程是边编译边执行的,
它不同于c,c++等语言,是将代码全部编译后,再统一执行。
        1.首先js代码的执行的第一步是做词法语法分析。 
        2.然后进行预编译的第一个时间点,即javaScript代码执行之前的预编译。
            在这个时间点,进行如下几个步骤。
                (1)创建全局对象(GO),填充内容 
                (2)创建全局执行上下文(GEC),并填充内容 
                (3)创建执行上下文栈(调用栈)ECS 
        3.接着开始执行代码,即从代码的第一行开始执行。 如果没有遇到函数,就一直执行。
        4.如果遇到函数时,会来到预编译过程的第二个时间点,即函数执行之前的时间点。 
            在这个时间点会进行如下的几个步骤。
                (1)将AO对象作为VO,赋值给FEC的VO结构。
                (2)填充作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
                (3)this绑定的值:这个我会在后续文章中详细解析; 
        5.执行函数体,知道该函数执行完毕 
        6.按照上述步骤继续执行全局代码,直到代码执行完毕。

什么是作用域?

        作用域可以简单的理解就是代码运行的一个区域,一个边界或者说是代码运行的环境。这个环境中的内容决定了代码可以访问
那些数据,这些数据可以是变量也可以函数。如果了解js代码的运行逻辑的话,我们就会知道其实作用域就是js引擎编译过程中生成的
VO对象,这个VO是一个变量,它可以被赋值为GO,AO,GO代表全局作用域,AO代表函数作用域。

js都有哪几种作用域?

        在es6之前,函数只有两种作用域,全局作用域和局部作用域(或者说函数作用域,个人认为函数作用域更适合)。在es6和
之后,添加了块级作用域。本文的探讨不涉及块级作用域,请各位看官知晓。所以js中截止目前拥有3种作用域
        (1)全局作用域
            js引擎在代码执行之前会生成一个全局对象,简称GO,这个GO就是我们的全局作用域。此时VO=GO
        (2)局部作用域(函数作用域)
            js引擎在执行函数前,首先会对这个函数进行预编译,生成AO,这个AO就是局部作用域或成为函数作用域。此时VO=AO。
        (3)块级作用域(es6及之后)
    

什么是作用域链?

    作用链是我们在查找一个变量或者函数的来源时,查找这个变量或者函数所经历的所有的作用域链接起来的链条。js代码在解析
执行的这个过程中,会生成执行上下文,js引擎添加其对应的作用域属性,这个属性的值由当前的作用域和其父级作用域组成。而一个
变量的作用域链就是从这个变量所处的作用域开始,通过其父级作用域一直延伸到全局作用域的一个链条。

全局作用域中的变量的提升和函数的提升

    这里的提升发生在js代码执行之前。
        (1)js代码在执行之前,会对全局的代码进行预编译,并生成一个全局对象GO,这个全局对象就可以理解为全局作用域。
        (2)此时会把全局的变量和函数的声明放在全局对象中,这个过程就是全局作用域中的函数的提升和变量的提升的过程。
        (3)同时在这个过程中也会生成全局上下文GEC,会有对GEC的作用域属性进行赋值,就是全局作用域本身。

局部作用域中的变量的提升和函数的提升

    这里的提升发生在函数被执行之前。
      (1)js代码在执行函数之前,先会对这个函数进行编译,在这个编译的过程中,会生成AO对象,这个AO对象就是局部作用域
或者说是函数作用域。
      (2)在生成AO的过程中,会把这个函数中声明的变量和函数提升至这个作用域的顶部,这个过程就是局部作用域中的变量的提升
和函数的提升。
      (3)同时在这个过程中,也会生成函数执行上下文FEC,并将这个函数作用域和其父级作用域赋值给FEC的作用域属性。

总结

    了解完上述的概念之后,相信大家对作用域的提升会有一个全新的理解,对于本文中的一些理解或用词不当的地方,欢迎大家在
    评论区指正。
    
    另外如果对本文中的一些概念和流程理解不清晰的地方可以去查看我对js代码的执行过程的解析文章。
        链接:[ js高级-js代码如何运行](https://juejin.cn/post/7090196102112083976)

几道作用域解析面试题

var n = 100
function foo(){
  n = 200
}
foo()

console.log(n)

1.js代码执行之前,首先会对全局代码进行编译,在此过程中会生成GO对象,具体结构如下
    GO = {
        ...js内置对象
        n:undefined
        foo = 0xa00(假设地址为0xa00)
    }

2.准备开始执行代码,此时会生成全局执行上下文(GEC),并将GO赋值给GEC,并给GEC的作用域属性赋值,最后将全局作用域中的代码
也赋值给GEC的执行代码属性。此时的GEC结构如下
    GEC = {
      VO = GO = {
        ...js内置对象
        n:undefined
        foo = 0xa00(假设地址为0xa00)
        }
       scoppechain:[GO]
       thisBinding:window
       code = {
           var n = 100
           foo()
           console.log(n)
       }
    }
    
3.创建执行上下文栈调用栈ECS,并将创建好的GEC放入ECS中执行。(这里ECS具体什么时间创建,我这里并不确定,只是确定此时
一定会有ECS)

4.放在全局代码中的代码开始一行一行进行执行。
    (1)首先是var n = 100,此时会将n的值赋值为100,此时GEC的结构为
         GEC = {
              VO = GO = {
                    ...js内置对象
                    n:100
                    foo:0xa00(假设地址为0xa00)
                }
               scoppechain:[GO]
               thisBinding:window
               code = {
                   var n = 100
                   foo()
                   console.log(n)
               }
          }

    (2)执行foo(),由于foo是一个函数,所以要对foo函数进行编译。
    
5.编译foo函数并执行
    (1)编译代码,生成AO对象,对应结构如下。
          AO = {
             null
          }
          因为在foo函数中没有声明任何变量和函数,只是使用了GO中的n
          
     (2)生成函数执行上下文FEC,并将函数的执行代码复制给FEC,FEC具体结构如下
         FEC = {
              AO = {     
              }
               scoppechain:[AO+parentScope(GO)]
               thisBinding:window
              code = {
                  n = 200
              }
         }
         
     (3)执行FEC的执行代码
         n = 200
         对n进行赋值时,首先会在自己的作用域进行查找,由于AO中没有对n变量的声明,所以会去当前作用域的父级作用域(GO)
中查找n, 所以查找到了GEC中的n,此时会将GEC中的n变量赋值为200。此时GEC的结构为
          GEC = {
              VO = GO = {
                    ...js内置对象
                    n:200
                    foo:0xa00(假设地址为0xa00)
                }
               scoppechain:[GO]
               thisBinding:window
               code = {
                   var n = 100
                   foo()
                   console.log(n)
               }
          }
          
6.执行全局代码的下一行代码
    console.log(n)
    由于这行代码是全局作用域中的代码,所以会在自己的作用域中查找,所以会在GO中查找n,此时GO中的n为200,所以会打印200.
所以代码的执行结果为
    200
    
    function foo() {
      console.log(n);
      var n = 200;
      console.log(n);
    }

    var n = 100;
    foo();
1.生成GO,结构如下
     GO = {
            ...js内置对象
            n=undefined
            foo = 0xa00(假设地址为0xa00)
      }
  
 2.生成GEC,结构如下
    GEC = {
      VO = GO = {
        ...js内置对象
        n=undefined
        foo = 0xa00(假设地址为0xa00)
        }
       scoppechain:[VO]
       thisBinding:window
       code = {
           var n = 100
           foo()
       }
    }

3.创建执行上下文栈调用栈ECS,并将创建好的GEC放入ECS中执行。

4.执行GEC中的全局代码
   (1)var n = 100,执行后GEC结构
    GEC = {
      VO = GO = {
        ...js内置对象
        n:100
        foo = 0xa00(假设地址为0xa00)
        }
       scoppechain:[VO]
       thisBinding:window
       code = {
           var n = 100
           foo()
       }
    }
    
 5.编译foo函数并执行
(1)编译代码,生成AO对象,对应结构如下。
      AO = {
         n:undefined
      }
 (2)生成函数执行上下文FEC,并将函数的执行代码复制给FEC,FEC具体结构如下
     FEC = {
          AO = { 
              n:undefined
          }
           scoppechain:[AO+parentScope(GO)]
           thisBinding:window
          code = {
              console.log(n);
              var n = 200;
              console.log(n);
          }
     }
  (3)执行FEC的执行代码
     第一行: console.log(n);
         对n进行打印时,首先会在自己的作用域进行查找,由于AO中有对n变量的声明,所以会使用当前作用域中的n, 所以为
     打印undefined。
     第二行: var n = 200
         会对FEC中的n进行赋值,此时GEC的结构为
              FEC = {
                  AO = { 
                      n:200
                  }
                   scoppechain:[AO+parentScope(GO)]
                   thisBinding:window
                  code = {
                      console.log(n);
                      var n = 200;
                      console.log(n);
                  }
             }
     第三行:console.log(n);
        对n进行打印时,首先会在自己的作用域进行查找,由于AO中有对n变量的声明,所以会使用当前作用域中的n, 因为n此时
     已经被赋值为200,所以为打印200。
         所以代码的执行结果为
            undefined 
             200
     
var n = 100
function foo1() {
  console.log(n) // 100,变量的提升,访问全局作用域中的n
}

function foo2(){
  var n = 200
  console.log(n) //200,当前函数作用域中的n
  foo1()

}

foo2()
console.log(n)  //100,当前作用域即全局作用域中的n
var a = 100
function foo() {
  console.log(a) //undefined,foo函数作用域中有a的声明,是在函数执行前进行变量的提升的,所以会访问当前作用域中的a
  return
  var a = 100
}

foo()

function foo() {
  var a = b = 10 // v8 引擎会将其解释为  var a = 10,b = 10 两行代码
}

foo();

console.log(a)   //此行会报错,因为访问不到全局作用域访问不到局部作用域中的a
console.log(b);  // v8 引擎会将b = 10 当作全局变量,进行变量提升,所以为打印10