干巴爹兔的博客 干巴爹兔的博客
首页
  • 前端文章

    • JavaScript
    • HTML
    • Vue
  • 学习笔记

    • JavaScript教程
    • React学习笔记
    • Electron学习笔记
  • 开源项目

    • cloud-app-admin
    • 下班了吗Vscode插件
    • Subversion变更单插件
  • Server

    • Django
  • 学习笔记

    • MySQL学习笔记
  • 运维

    • 服务器部署
    • Linux
  • 日常学习

    • 学习方法
关于
收藏
友链
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

干巴爹兔

卑微的前端打工人
首页
  • 前端文章

    • JavaScript
    • HTML
    • Vue
  • 学习笔记

    • JavaScript教程
    • React学习笔记
    • Electron学习笔记
  • 开源项目

    • cloud-app-admin
    • 下班了吗Vscode插件
    • Subversion变更单插件
  • Server

    • Django
  • 学习笔记

    • MySQL学习笔记
  • 运维

    • 服务器部署
    • Linux
  • 日常学习

    • 学习方法
关于
收藏
友链
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 理解JS作用域

    • 理解JS作用域笔记(一):五个阶段、词法作用域
      • 理解JS作用域笔记(完):声明提升、作用域链
    • JS面向对象

    • 《JavaScript教程》笔记
    • 理解JS作用域
    干巴爹兔
    2020-07-06
    目录

    理解JS作用域笔记(一):五个阶段、词法作用域

    # 开头

    笔记视频内容源自B站JavaScript从入门到放弃 第九章 深入理解JS作用域 (opens new window),笔记为自行整理复习使用,欢迎一同学习交流,转载请通知原作者

    # 一、内部原理

    JS中的作用域分为两个作用域,一个是全局作用域,一个是函数作用域,JS引擎有着一套良好的规则去存储变量,并能够很方便的去找到变量,举一个例子:

    <body>
        <script>
          var a = 2;
          console.log(a);
          function add() {
            var b = 3;
            console.log(a);
          }
          console.log(b);
        </script>
      </body>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    a变量作为全局作用域,它能够在函数下被找到,这套规则就叫做作用域,b作为函数作用域下的变量无法在函数外被找到。理解好作用域也对后续理解this有做帮助。JS代码没有编译阶段,是一种边解释边执行的语言。

    作用域内部可以划分为五个阶段(了解):

    1. 编译
    2. 执行
    3. 查询
    4. 嵌套
    5. 异常

    # 1.1、编译阶段

    我们以一个代码为例子:

    <script>
        var a = 2;
    </script>
    
    1
    2
    3

    我们将编译阶段划分成三个步骤:分词(分成不同的词法单元)、解析、代码生成

    分词首先将语句分为一个个的词法单元:

    var , a , = , 2, ;
    
    1

    在解释阶段内部将其放入一个数组或对象:

    {
        "var":"keyword",//关键字
         "a":"indentifier",//标识符
         "=":"assignment",//分配
         "2":"interger",//整数
         ";":"eos",//(end of statement)结束语句
    }
    
    1
    2
    3
    4
    5
    6
    7

    解析功能将对应的属性转化为一个抽象语法树(AST Abstract Snatax Tree)

    image-20200706194645685

    代码生成将抽象语法树转化为可执行的代码的过程,转化成一组机器指令,我们称这个过程为代码生成

    # 1.2、执行阶段

    代码生成后就来到了执行阶段,JS是边解释边执行,以一段代码举例子:

    <script>
        var a = 2;
        console.log(a);
        console.log(b);
    </script>
    
    1
    2
    3
    4
    5

    1、JS引擎执行时首先会查找作用域,首先查找a是否在当前作用域下,如果是,引擎就会直接使用这个变量。如果不在当前作用域则会继续往外找,直到找不到为止

    2、如果找到了变量,就会将 2赋值给当前的这个变量,否则引擎就会抛出异常,就像代码示例中的b未定义,控制台抛出了异常

    image-20200706195646513

    # 1.3、查询阶段

    先看代码:

    <body>
        <script>
            var a = 2;
        </script>
    </body>
    
    1
    2
    3
    4
    5

    查询阶段,引擎查询a变量,这样的查询叫做LHS(Left hand Side)左查询,RHS(Right hand Side)右查询用在函数的调用,比如:

    <script>
        var a = 2;
        function add(){
        }
         add();
    </script>
    
    1
    2
    3
    4
    5
    6

    当变量出现在赋值操作的左侧时,查询为LHS,出现在右侧时如函数调用就为RHS

    看一个例子:

    function foo(a) {
        console.log(a);
        }
    foo(2);
    
    1
    2
    3
    4

    1、foo(2)对function函数对象做了一个RHS引用,这种引用是一种查询引用

    2、函数传参a=2对a变量进行了LHS引用

    3、console.log(a);对console对象进行RHS引用,并检查console中是否有log方法

    4、console.log(a);对a进行RHS引用,并把得到的结果传给了console.log(a);

    # 1.4、嵌套阶段

    嵌套阶段是一个非常重要的阶段,嵌套机制是根据当前作用域进行查找的机制,先看代码:

    <body>
        <script>
            //作用域变量的查找机制(重要)
            //在当前作用域下无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量或者是抵达最外层作用域(全局作用域)为止
          function foo(a) {
            console.log(a + b);
          }
          var b = 2;
          foo(4);
        </script>
      </body>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    JS引擎在执行这句话时,经历了前三个阶段后,在当前的这个作用域下去寻找b,找不到的话会向外层(全局嵌套的作用域)寻找b变量。

    image-20200706201249974

    这里使用到了变量提升,js引擎会判断var变量,将其提升到最前面,所以不用去在意变量声明的位置

    <body>
        <script>
          //作用域变量的查找机制(重要)
          function foo(a) {
            console.log(a + b);
            function fo() {
              console.log(a + b);
            }
          }
          var b = 2;
          foo(4);
        </script>
      </body>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    这个代码同样的思路,无论嵌套多深,寻找作用域都是从内而外去寻找,直到全局作用域下没发现位置,报错

    # 1.5、异常阶段

    异常就是在浏览器上出现的一些错误

    例子1:

    <body>
        <script>
            function fn(a){
                a = b;
            }
            fn(2);
        </script>
    </body>
    
    1
    2
    3
    4
    5
    6
    7
    8

    image-20200706202243744

    我们能发现b is not defined,因为b是一个未声明的变量,在当前作用域的RHS查询时未找到,在全局作用域也找不到,所以输出了错误

    例子2:

    function fn2(){
        var b = 0;
        b();
    }
    
    1
    2
    3
    4

    image-20200706202220159

    例子3:

    function fn(){
       a = 1;
    }
    fn();
    console.log(a);
    
    1
    2
    3
    4
    5

    如果fn()这一行被注释掉就会引起报错:a is not defined,加上这一行则不会报错,这是因为执行函数时,引擎执行了LHS查询,将1赋值给了a变量,如果在该函数中没有声明a这个变量,引擎将会在全局作用域中声明一个a变量并进行赋值操作,所以外层作用域下的log方法能够访问到a这个变量

    但是在严格模式下就无法进行这种操作:

    function fn(){
       'use strict';
        a = 1;
    }
    fn();
    console.log(a);
    
    1
    2
    3
    4
    5
    6

    image-20200706202822838

    所以一般在使用函数时都会加上这句话,防止一些非法操作

    # 二、词法作用域

    # 2.1、作用域查找机制

    作用域分为两种,一个是词法作用域,一个是动态作用域,JS中不存在所谓的动态作用域,所以我们通过一个案例来看看词法作用域

    <body>
        <script>
          function foo(a) {
            var b = a * 2;
            function bar(c) {
              console.log(a, b, c);
            }
            bar(b * 3);
          }
          foo(2);
        </script>
      </body>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    image-20200706204047251

    首先外层有一个作用域,foo函数有一个函数作用域,bar也有一个作用域,有三层作用域。从作用域的角度分析,我们将代码分为三层作用域,最外层全局作用域1,foo函数作用域2,bar函数作用域3,1包含2包含3

    image-20200706204234986

    首先1全局作用域下找到了一个foo(词),在2的作用域下找到了b、a、bar(三个词),3下只有一个c(词)

    image-20200706204418110

    执行foo时,2赋值给a,a变量是属于foo作用域的,foo作用域内有找到a就赋值给它,bar函数把b*3的值赋值给c,来到bar的作用域,发现a并不在当前bar函数的作用域下,于是去它的外层作用域(foo)查找,找到了a,再去找b,b不在当前作用域于是往外层(foo)寻找。最后再去找c,c在当前作用域下(bar)就赋值。也就是说作用域是由代码里函数声明时决定的,你在哪里声明,他所在的执行上下文就被决定了,通过词法作用域才可以预测代码在执行过程中如何查找标识符。

    # 2.2、遮蔽效应

    什么是遮蔽,作用域查找从运行时所处的最内部作用域开始,逐级向上进行,直到遇到第一个匹配的标识符为止

    在多层的嵌套作用域中可以定义同名字的标识符,这叫做遮蔽效应

    <body>
        <script>
          var a = 0;
          function test() {
            var a = 1;
            console.log(a);
          }
          test();
        </script>
      </body>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    在进入test函数作用域时首先查找a变量是否在当前作用域,发现第一个匹配的就不会再去向外匹配查询,这样内层作用域就覆盖了外层的变量,这就是遮蔽效应

    所以全局声明的变量要小心函数中再去声明相同的变量,以免遮蔽效应的产生。

    编辑 (opens new window)
    #JavaScript
    上次更新: 2022/08/26, 15:52:02
    理解JS作用域笔记(完):声明提升、作用域链

    理解JS作用域笔记(完):声明提升、作用域链→

    最近更新
    01
    使用Vscode开发一个小插件
    10-21
    02
    Vscode插件配置项监听
    10-18
    03
    使用has属性构造必填效果
    10-14
    更多文章>
    Theme by Vdoing | Copyright © 2020-2023 互联网ICP备案: 闽ICP备18027236号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式