深入理解JavaScript——作用域

121 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

作用域

先来引入这个概念:

为什么要有作用域?

编程语言最基本的功能:就是要能够储存变量当中的值,并且能在之后对这个值进行访问或修改。

所以作用域这个概念就引申出来了,我们设计了一套规则来存储变量,而这套规则就被称为作用域。

编译原理

下面我们来了解下JavaScript是怎样编译程序的,有助于我们后面对作用域的理解。

程序在执行前,需要被编译,编译又分为三个阶段,分别是:词法分析、语法分析、代码生成

词法分析

也叫分词(Tokenizing),这么说吧,为什么又会有这样的叫法呢

简单来说,区别就在于词法单元的识别是否是有状态的

有状态的识别称为词法分析,无状态的识别称为分词

var b = 13会被分解为

[    {        "type": "Keyword",        "value": "var"    },    {        "type": "Identifier",        "value": "b"    },    {        "type": "Punctuator",        "value": "="    },    {        "type": "Numeric",        "value": "13"    },    {        "type": "Punctuator",        "value": ";"    }]

Keyword(关键词) |

Identifier(标识符) | Punctuator(标点符号) | Numeric(数字)

最终会被解析为词法单元流,以数组的形式

语法分析

也叫解析(Parsing),这个过程是一个:数组 => 树 的过程

树是抽象语法树(Abstract Syntax Tree)

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "b"
          },
          "init": {
            "type": "Literal",
            "value": 13,
            "raw": "13"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "script"
}

代码生成

接下来需要将AST转化为可执行代码

Javascript引擎和编译器和作用域

上面讲了在执行代码前需要编译,中间会声明变量

那么这些变量就是储存在作用域中(scope)

作用域又分为函数作用域和全局作用域

所以我们要运行一段JavaScript代码时,JavaScript引擎会通过查找作用域中的变量来执行代码。

编译器通过三个步骤:分词、解析、生成代码

然后引擎执行代码,通过作用域的协助,进行LHS查询和RHS查询

这里不具体解释,通过例子来理解这两个查询

下面来看个例子帮助了解

function foo(a) {
    console.log( a )
}
​
foo( 2 );

当执行以下代码的时候,(在执行前很短很短的时间,JavaScript编译器才将这段代码编译好,生成执行代码)

引擎先在全局作用域中查看是否有函数foo的声明,进行RHS查询,

foo(2)中的2,相当于a = 2,要给a赋值上2,需要进行LHS查询,

然后进入到函数foo内部,console.log 对console对象进行RHS查询

再对a进行RHS查询

每次引擎进行查询(对变量、对象、函数),都是看作用域中是否有声明

作用域

JavaScript有两种工作模式:

  1. 词法作用域
  2. 动态作用域

JavaScript 支持词法作用域,在树状嵌套结构中代码块创建出新的作用域。

// global scopefunction scopeOne() {
  // scope 1function scopeTwo() {
    // scope 2
  }
}

在 JavaScript 中,每当你创建了一个引用,不管是通过变量(variable)、函数(function)、类型(class)、参数(params)、模块导入(import)还是标签(label)等,它都属于当前作用域。

var global = "I am in the global scope";
​
function scopeOne() {
  var one = "I am in the scope created by `scopeOne()`";
​
  function scopeTwo() {
    var two = "I am in the scope created by `scopeTwo()`";
  }
}

更深的作用域可以引用外部作用域的声明的变量

function scopeOne() {
  var one = "I am in the scope created by `scopeOne()`";
​
  function scopeTwo() {
    one = "I am updating the reference in `scopeOne` inside `scopeTwo`";
  }
}

内层作用域也可以创建和外层作用域同名的引用

function scopeOne() {
  var one = "I am in the scope created by `scopeOne()`";
​
  function scopeTwo() {
    var one = "I am creating a new `one` but leaving reference in `scopeOne()` alone.";
  }
}

根据上面的例子,我们时常可以看到一个函数或一个块嵌套在另一个函数或者块当中

如果引擎在当前作用域不能找到变量,就会往外层作用域去查找

引擎会先在当前作用域去查找,LHS和RHS本质上都可以理解为:找变量。

对一个还没有声明的变量而言,也就是说:在任何作用域都找不到

以下两个例子都是基于还没有声明的变量来说的

进行RHS查询时,会产生ReferenceError的异常

进行LHS查询时,却并不会产生异常,而是会返回undefined(非严格模式下)

(当然这里的b输出是没问题的为2)

小结

LHS查询是:查找的目的是对变量进行赋值

RHS查询是:查找的目的是获取变量的值

编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来


参考文章:

JavaScript深入之词法作用域和动态作用域

你不了解的JavaScript