作用域

几乎所有编程语言最基本的功能之一,就是能够存储变量当中的值,并且能在之后对这个值进行访问和修改。事实上,正是这种存储和访问变量的能力将状态带给了程序。如果一个程序没有状态,它是没有灵魂的。

但是,将变量引入程序会引起几个有趣的问题:这些变量存储在哪里?程序需要的时候如何找到它们?这些问题说明需要一套规则来存储变量,并且可以方便地找到这些变量,这套规则就被称为作用域。

编译原理 

在讨论作用域之前,先来聊聊编译原理。啥玩意?编译原理??你可能会问:JS 不是一个解释型语言吗?编译个啥?

然而,JS 的编译比传统的编译要复杂的多,而且,它不是提前编译的,编译结果不能在分布式系统进行移植。

在传统编译语言中,程序中的一段源代码在执行之前都会经历三个步骤,这三个步骤统称为编译。

1. 分词/词法分析

这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块也称为代码单元。

分词和词法分析之间的区别是非常微妙的,主要差异在于词法单元的识别是通过有状态还是无状态的方式进行的。简单来说,如果词法单元生成器在判断 a 是一个独立的词法单元还是其他词法单元的一部分时,调用的是有状态的解析规则,那么这个过程就是词法分析。

2. 解析/语法分析

 这个过程将词法单元流(数组)转换成一个由元素逐级嵌套组成的代表了语法结构的树,这个树被称为:语法树。

 例如:var a = 2; 的抽象语法树可能有一个叫 VariableDeclaration 的顶级节点,接下来一个叫 Identifier 的子节点,以及一个 AssignmentExpression 的子节点。

3. 代码生成

将 AST 转换为可执行代码的过程。抛开具体的细节,简单来说就是有某种方法可以将 var a = 2; 的 AST 转换为一组机器指令,用来创建一个 a 变量,并将对应的值存储在变量中。

然而,上面这是传统的编译语言的编译过程,该死的 JS 编译过程要远远比上面的复杂。对于 JS 来说,大部分情况下编译发生在执行前的几微秒,甚至更短的时间内。

三个家伙 

 额。。不好意思,本文的主题其实是作用域,但是因为作用域、编译器和引擎三者是不是家的,抛开来谈似乎没有意义,所以在此之前先来看看三者。

  • 引擎 —— 负责整个 JS 程序的编译及执行过程。
  • 编译器 —— 负责语法分析以及代码生成。
  • 作用域 —— 负责收集并维护由所有声明的标识符组成的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些标识符的访问权限。

 引擎

编译是程序执行前的一个过程,然而,在这个过程里面统筹一切工作的家伙就是引擎。准确的说,引擎负责整个 JS 程序的编译以及执行过程。

为了更好的剖析,我们从一句代码:var a = 2; 来分析。

从开发者的角度来看,这无疑是一句简单的代码。但是,在引擎的角度来看就不是那么简单了。事实上,引擎认为这里面有两个完全不同的声明,一个由编译器在编译时处理,另一个由引擎在运行时处理。

编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树状结构。但是,当编译器开始进行代码生成时,它对这段程序的处理方式和预期的有所不同。具体的处理如下:

  1. 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在当前作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前的作用域集合(作用域的变量对象)声明一个新的变量,名为 a。
  2. 接下来,编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时首先会询问作用域,在当前的作用域集合是否存在一个 a 变量,如果是,引擎就会使用这个变量;否则,引擎会沿着作用域链继续查找该变量。最后,找到 a 变量则进行赋值;否则抛出异常 a is not defined。

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量,然后在运行时引擎会在作用域中查找该变量,如果能找到则对其进行赋值操作。

从上面来看,一个变量的声明是作用域来处理的;一个变量的赋值操作是由引擎来处理。但是无论作用域还是引擎,都有查询变量的能力。对于作用域而言,它只能查询当前(自身)的变量对象;但是对于引擎而言,它能沿着作用域链查找。

引擎查找变量的方式有两种:LHS 与 RHS

编译器经过分词和解析的过程以后就会生成代码,引擎执行代码时,会通过查找变量来判断它是否已声明。查找的过程由作用域进行协助,但是引擎执行怎样的查找,会影响最后的查找结果。

在前面的例子中,引擎会为变量 a 进行 LHS 查询(LHS 和 RHS 的区别就是赋值操作的左侧和右侧)。RHS 查询与简单的查找某个变量的值别无二致(沿着作用域链查找);而 LHS 查询则是试图找到变量的容器本身,从而可以对其进行赋值。这两种方式的区别还有一点:

RHS 是找到一个变量的值,如果在当前作用域没有找到,则往上级作用域查询,一直到全局作用域,最后没有找到则会抛出异常;LHS 则不会抛出异常,取而代之的是创建一个变量。

词法作用域

在前面提到,作用域就是一套规则,这套规则用来管理引擎如何在当前的作用域链中根据标识符进行变量查找。作用域一共有两种工作模型,第一种是最为普遍的词法作用域,另一种是动态作用域。

编译器的第一个工作就是词法化,将字符串分解成有意义的代码单元,也称为词法单元。所以,词法化这个过程会对源码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。

词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域就是由你在写代码时将变量和块级作用域写在哪里决定的,因此,当词法分析器处理代码时会保持作用域不变。在 JS 中,我们使用的工作模型都是属于词法作用域。

执行环境与作用域链

执行环境,简称环境,是 JS 中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。但是无法通过编码的方式访问这个对象。

全局执行环境是最外围的一个执行环境。根据 ES 实现所在的宿主环境不同,表示执行环境的对象也不一样。在浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。全局作用域直到应用程序退出(比如关闭网页或浏览器)才会销毁。

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中(压栈)。而在函数执行之后,栈将其环境退出(出栈),把控制权返回之前的执行环境。

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。作用域链中的下一个变量对象来自包含外部环境,而再下一个变量对象则来自下一个包含环境。这样,一致延续到全局执行环境,全局执行环境的变量对象始终都是作用域链中的最后一个对象。

所以,作用域链的起点就是当前的变量对象,终点就是全局环境的变量对象,查找某个变量从起点的变量对象开始查询,一直到全局环境的变量对象结束。中途如果能查找到,就会返回;如果到全局环境的变量对象都没有这个变量,则会抛出异常。

最后,还需要注意的一种情况是,如果多个执行环境的变量对象都保存着一个相同的标识符,那么返回的是距离当前作用域链最近的变量对象里面的那个标识符,在其他节点上的都会被屏蔽掉,这种现象也被称为屏蔽效应。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值