搜索
写经验 领红包
 > 游戏

如何理解作用域(作用域的概念)

导语:理解作用域

如何理解作用域(作用域的概念)

我们学习作用域的方式是将这个过程模拟成几个人物之间的对话。那么,由谁进行这场对 话呢?

演员表

首先介绍将要参与到对程序 var a = 2; 进行处理的过程中的演员们,这样才能理解接下来 将要听到的对话。

引擎:从头到尾负责整个 JavaScript 程序的编译及执行过程。编译器:引擎的好朋友之一,负责语法分析及代码生成等脏活累作用域:引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查 询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

为了能够完全理解 JavaScript 的工作原理,你需要开始像引擎(和它的朋友们)一样思考, 从它们的角度,并从它们的角度回答这些问题。

对话

当你看见 var a = 2; 这段程序时,很可能认为这是一句声明。但我们的新朋友引擎却不这 么看。事实上,引擎认为这里有两个完全不同的声明,一个由编译器在编译时处理,另

个则由引擎在运行时处理。

下面我们将 var a = 2; 分解,看看引擎和它的朋友们是如何协同工作的。

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

可以合理地假设编译器所产生的代码能够用下面的伪代码进行概括:“为一个变量分配内 存,将其命名为 a,然后将值 2 保存进这个变量。”然而,这并不完全正确。

事实上编译器会进行如下处理。

遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的 集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作 用域的集合中声明一个新的变量,并命名为 a。 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值 操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。

如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会举手示意并抛出一个异常!

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如 果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对 它赋值。

为了进一步理解,我们需要多介绍一点编译器的术语。

编译器在编译过程的第二步中生成了代码,引擎执行它时,会通过查找变量 a 来判断它是 否已声明过。查找的过程由作用域进行协助,但是引擎执行怎样的查找,会影响最终的查 找结果。

编译器

在我们的例子中,引擎会为变量 a 进行 LHS 查询。另外一个查找的类型叫作 RHS。

我打赌你一定能猜到“L”和“R”的含义,它们分别代表左侧和右侧。

什么东西的左侧和右侧?是一个赋值操作的左侧和右侧。

换句话说,当变量出现在赋值操作的左侧时进行 LHS 查询,出现在右侧时进行 RHS 查询。

讲得更准确一点,RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图 找到变量的容器本身,从而可以对其赋值。从这个角度说,RHS 并不是真正意义上的“赋 值操作的右侧”,更准确地说是“非左侧”。

你可以将 RHS 理解成 retrieve his source value(取到它的源值),这意味着“得到某某的 值”。

让我们继续深入研究。

比如console.log( a );

其中对 a 的引用是一个 RHS 引用,因为这里 a 并没有赋予任何值。相应地,需要查找并取 得 a 的值,这样才能将值传递给 console.log(..)。

相比之下,例如:

a = 2;

这里对 a 的引用则是 LHS 引用,因为实际上我们并不关心当前的值是什么,只是想要为 = 2 这个赋值操作找到一个目标。

LHS 和 RHS 的含义是“赋值操作的左侧或右侧”并不一定意味着就是“= 赋值操作符的左侧或右侧”。赋值操作还有其他几种形式,因此在概念上最 好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头 (RHS)”。

考虑下面的程序,其中既有 LHS 也有 RHS 引用:

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

foo( 2 );

最后一行 foo(..) 函数的调用需要对 foo 进行 RHS 引用,意味着“去找到 foo 的值,并把 它给我”。并且 (..) 意味着 foo 的值需要被执行,因此它最好真的是一个函数类型的值!

这里还有一个容易被忽略却非常重要的细节。

代码中隐式的 a=2 操作可能很容易被你忽略掉。这个操作发生在 2 被当作参数传递给 foo(..) 函数时,2 会被分配给参数 a。为了给参数 a(隐式地)分配值,需要进行一次 LHS 查询。

这里还有对 a 进行的 RHS 引用,并且将得到的值传给了 console.log(..)。console. log(..) 本身也需要一个引用才能执行,因此会对 console 对象进行 RHS 查询,并且检查 得到的值中是否有一个叫作 log 的方法。

最后,在概念上可以理解为在 LHS 和 RHS 之间通过对值 2 进行交互来将其传递进 log(..) (通过变量 a 的 RHS 查询)。假设在 log(..) 函数的原生实现中它可以接受参数,在将 2 赋 值给其中第一个(也许叫作 arg1)参数之前,这个参数需要进行 LHS 引用查询。

你可能会倾向于将函数声明 function foo(a) {... 概念化为普通的变量声明 和赋值,比如 var foo、foo = function(a) {...。如果这样理解的话,这 个函数声明将需要进行 LHS 查询。 然而还有一个重要的细微差别,编译器可以在代码生成的同时处理声明和值 的定义,比如在引擎执行代码时,并不会有线程专门用来将一个函数值“分 配给”foo。因此,将函数声明理解成前面讨论的 LHS 查询和赋值的形式并 不合适。

本文内容由小悦整理编辑!