JavaScript是一门编译语言,在编译的流程中,程序中的一段源代码在执行之前会经历
分词/词法分析
、解析/语法分析
和代码生成
三个步骤,统称为编译
作用域
一套设计良好的规则来存储变量,并且之后可以方便找到这些变量。这套规则被称为作用域。
在学习作用域之前先引入一个概念
- 引擎
从头到尾负责整个JavaScript程序的编译及执行过程。 - 编译器
引擎的好朋友之一,负责语法分析及代码生成等脏活累活。 - 作用域
引擎的另一位好朋友,负责收集维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
当我们看到var a = 2;
这段程序时,很可能认为这是一句声明。但我们的新朋友引擎却不这么看。事实上,引擎认为这里有两个完全不同的声明,一个由编译器在编译时处理,另一个则由引擎在运行时处理。
我们可以将var a = 2;
分解,看看引擎和它的朋友们是如何协同工作的。
编译器会进行如下处理:
- 遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域集合中声明一个新的变量,并命名为a。
- 接下来编译器会在引擎中生成运行时所需的代码,这些代码被用来处理
a = 2
这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫做a的变量,如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。如果引擎最终找到了a变量,就会将2赋值给它,否则引擎就会举手示意并抛出一个异常!
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
词法作用域
我们将“作用域”定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。
作用域共有两种工作模式。第一种是最为普遍的,被大多数编程语言所采用的词法作用域。另一种叫做动态作用域,仍有一些编程语言在使用。
词法作用域的定义:
简单地说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)
既然大部分情况下是这样的,但是会有一些欺骗词法作用域eval
和with
,有兴趣的小伙伴可以去自己去学习下,这里就不做过多的描述
var a = 'outer';
function foo() {
var a = 'fooInner';
bar();
}
function bar () {
console.log(a);
}
foo(); // 'outer'
看上面的程序,在没去运行之前,你觉得最终的结果是多少呢?看到答案之后,会不会对词法作用域有一些自己的见解呢?可以结合定义和这段程序好好思考下,可能会对词法作用域有个更深的理解
到现在为止,你应该已经熟悉作用域的概念,以及根据声明的位置和方式将变量分配给作用域的相关原理了。可以总结为:任何声明在某个作用域内的变量,都将附属于这个作用域。
但是作用域同其中的变量声明出现的位置有某种微妙的联系,而这个细节正是我们将要讨论的内容。
直觉上会认为JavaScript代码在执行时由上而下一行一行执行的。但实际上并不完全正确
提升
考虑以下代码:
a = 2;
var a;
console.log(a);
最终的结果是什么呢?
你会觉得是undefined??但是,真正的结果是2
考虑另外一段代码:
console.log(a);
var a = 2;
最终的结果是什么呢?
你会觉得是2??还是会抛出ReferenceError 异常??但是,这两种结果都不对。输出来的是undefined
如果你对这些结果不是很明白,可以再看一遍这篇文章开始描述的作用域。引擎会在解释JavaScript代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用适合的作用域将它们关联起来。
因此,正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先
被处理。
当你看到var a = 2; 这个代码的实际执行过程:
var a; // 第一步:编译
a = 2; // 第二步:执行
console.log(a);
var a = 2;
类似地,这个代码实际是按照以下流程处理的:
var a;
console.log(a);
a = 2;
这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。这个过程就叫做提升
换句话说,先声明,后赋值。
只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
foo函数的声明被提升了,而且函数内部的变量也提升了,因此第一行中的调用可以正常执行。
每个作用域都会进行提升操作
。foo(..)函数自身也会在内部对var a进行提升(显然并不是提升到了整个程序的最上方)。因此这段代码实际上会被理解为下面的形式:
function foo() {
var a = 1;
console.log( a ); // undefined
a = 2;
}
foo();
函数声明会被提升,但是函数表达式却不会被提升。
foo(); // 报错:TypeErrro!(不是ReferenceError)
var foo = function bar () {
// ...
}
函数优先
函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个“重复”声明的代码中)函数会首先被提升,然后才是变量。
考虑以下代码:
foo(); // 1
var foo;
function foo () {
console.log(1);
}
foo = function () {
console.log(2);
}
结果输出是1!这个代码片段会被引擎理解为如下形式:
function foo () {
console.log(1);
}
foo();
foo = function () {
console.log(2);
}
注意:var foo尽管出现在function foo()...的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。尽管重复的var 声明会被忽略掉,但是出现在后面的函数声明还是可以覆盖前面的。
网友评论