什么是作用域

513 阅读5分钟

几乎所有的编程语言都有作用域的这个理念,我们可以理解成存储变量当中的值,并且能够对当中值进行访问或是修改。

神奇的编译原理

  1. 词法分析 Tokenizing 如何理解这个过程呢? 其实这个过程是由字符组成的字符串被分解成对编程语言来说是有意义的代码块。 比如
var z = 2; 

其实是被分解成词法单元 var、z、=、2、 然而空格会不会被当成词法单元,完全是取决于空格是否具有意义。

  1. 语法分析 Parsing 什么是语法分析呢?我们大可不必想的很复杂,语法分析这个过程最终会将词法单元流,也可以理解成数组转换成一个由元素挨个嵌套组成程序语法结构树,当然我们耳熟能详的AST,便是前者提到的语法结构树(Abstrack Syntax Tree) 。

  2. 代码生成 我们可以把AST转换为可执行代码或者计算机程序能够识别的过程,理解成代码生成,简而言之,var z = 2; 在AST的转化过程中,创建了一个z的变量,然后给它分配内存,并且将2存储在z中。

如何理解作用域

我们可以把它归类成引擎、编译器、作用域三个部分。

引擎

它负责将整个JavaScript程序的编译以及执行过程

编译器

编译器和引擎是相辅相成的,在整个编译过程中充当着语法分析和代码生成的角色

作用域

作用域同样在引擎和编译器的配合下是不可缺少的一部分,在这个过程中,作用域的职责是收集和维护被声明的变量,然后组成一系列的查询,并且按照一套严格的查询规则,确保当前执行的代码对变量的访问权限

划分作用域
  • 词法作用域: 在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸
  • 动态作用域: 在代码运行时完成划分,作用域链沿着它的调用栈往外延伸
修改词法作用域
  1. eval 对作用域的修改
function tips(str) {
 eval(str)
 console.log(name)
}

var name = 'GleenLey'
var str = 'var name = "lee"'

tips(str) // 输出 lee
eval 函数的入参是一个字符串。当 eval 拿到一个字符串入参后,
它会将这段字符串的内容当做一段 js的代码,插入到被调用的位置,
所以上面代码例子里,被 eval “改造” 过后的函数其实就是这样。
此时当我们输出 name 的时候, 函数作用域内的 name 已经被 eval 
传入的这行代码给修改掉了,所以作用域内 name 的值就从 ‘GleenLey’ 
变成了 ‘lee’(eval 带来的改变如下图所示)。而这个改变确实只有在
eval (str) 这行代码被执行后才发生 ——eval 在运行时改变了作用域的内容,
它成功地 “修改” 了词法作用域规则约束下在书写阶段就划分好的作用域。

eval.png 2. with 对作用域的修改

with 对比 eval 我们可能要陌生一些。它的作用其实就是帮我们 “偷懒”,
当我们不想重复的写一个对象名作为前缀的时候,with 可以帮到我们提供这样的便利:
var me = {
  name: 'GleenLey',
  career: 'coder',
  hobbies: ['coding', 'basketball']
}

// 假设我们想输出对象 me 中的变量,没有 with 可能会这样做:
console.log(me.name)
console.log(me.career)
console.log(me.hobbies)

// 但是 with 可以帮我们省去前缀
with(me) {
  console.log(name)
  console.log(career)
  console.log(hobbies)
}
// with 就是当我们期望引用一个对象内的多个属性的时候,一个 “偷懒” 的办法
  • with 会原地创建一个全新的作用域,这个作用域内的变量集合,其实就是传入 with 的目标对象的属性集合。
  • 因为 “创建” 这个动作,是在 with 代码实际已经被执行后发生的,所以这个新作用域确实是在运行时被添加的, with 因此也实现了对书写阶段就划分好的作用域进行修改。

这里面需要注意的是,“改变” 仅仅是描述 “创建” 这个动作 —— 创建出来的这个新的作用域。因此它的作用域查询机制仍然是遵循词法作用域模型的。

tips:千万不要用 with 和 eval 写代码

什么是LHS和RHS

顾名思义 L便是代表 Left R便是代表 Right。 其实我们可以理解成: 当变量出现在赋值操作的左侧的时候,便是LHS查询,反之便是RHS查询。

比如:

console.log(z);

在寻找a所持有的内存地址的时候,这个过程就是RHS查询,可以理解成RHS引用。

再比如:


function bar(z){
  console.log(z); // 3
}

bar(3);  

在这个代码块中,不仅出现了RHS查询,也出现了LHS查询。 其实作用域我们可以把它理解成一套规则,用来确定在何处以及如何去查找变量,如果查找的目的是对变量(标识符)进行赋值,那么就会进行LHS查询,如果目的是获取变量的值,那么就是RHS查询。

作用域的查找规则

在多层嵌套作用域的时候,我们该如何理解查找规则呢? 其实这个规则很简单,我们只需要记住: 引擎会从当前的执行作用域开始去寻找变量,如果找没找到就会往上级去寻找,找到最外层的全局global作用域时,不管有没有找到,查询的过程都会停止。