wuhuihao
Frontend Developer
Hangzhou,Wenzhou

JavaScript中的变量作用域

在这篇文章中,我们通过几个例子来理解学习Javascript中的变量作用域,所有的代码在chrome浏览器中执行。

//code-1
var a = "a";

function fun(){
	var b = "b"
	console.log(a)
	console.log(b)
}

fun()
console.log(a)
concole.log(b)

code-1 代码段执行之后,我们获得了如下输出结果

a
b
a
Uncaught ReferenceError: b is not defined

在fun函数执行的时候,不论是在函数体内定义的变量b,还是在全局代码中定义的变量a,都能取到它们的值,而在fun函数执行完毕后,fun方法体中定义的变量b在全局代码中是无法取得的。

从以上代码的执行结果看来,在全局代码中定义的变量a的作用域覆盖了整个fun函数体,而在fun函数体中定义的变量的作用域只局限于fun的函数体,并没有覆盖到全局代码中。

下面再来看一段代码:

//code-2
var b = 123
function fun(){
	var a = 12;
    baz();
}

function baz(){
  console.log(b)
  console.log(a);
}
fun()

code-2 代码段执行之后,我们获得如下的输入结果

123
Uncaught ReferenceError: b is not defined

baz函数在fun函数内被调用,也就说baz函数在fun函数执行的时候被调用。我们尝试在baz函数内输出fun函数内定义的变量a,但是我们得到了报错的信息。也就是说变量的作用域是在函数定义的时候确定的,与函数执行的位置无关。

下面我们通过ECMAScript规范中的定义,去解释函数能拥有独立作用域的原因。

词法环境(Lexical Environments)

ECMAscript 规定了词法环境是一个用于定义特定变量和函数标识符在 ECMAScript 代码的词法嵌套结构上关联关系的规范类型。一个词法环境由一个环境记录项和可能为空的外部词法环境引用构成。

通常词法环境会与函数定义, catch代码块的句法结构相联系,比如说函数的定义语句的括号内会确定一个词法环境的范围,并且在代码执行的时候创建一个新的词法环境。

环境记录项

环境记录项记录了在其关联的词法环境范围中创建的标识符绑定。词法环境定义了一个范围,这个范围内定义的变量或函数会记录在改词法环境的环境记录项中。

外部词法环境引用

外部词法环境引用用于表示词法环境的逻辑嵌套关系模型。如果函数a和函数c是定义在函数b内部的,那么b函数和c函数的外部词法环境就是a函数定义语句执行时候创建的词法环境。

外部词法环境引用的作用是在标识符解析的过程中形成一条作用域链。比如在上一段所描述的三个函数,如果在b函数和c函数中找不到所期待的变量,会到上一级作用域,也就是外部词法环境引用的a函数的作用域中去寻找。

	var wang = "wang"
	
	function a(x){
		
		var jay = "jay";
		
		function b(y){
			var jj = "jj"
		}
		
		function c(z){
			var lee = "lee"
		}
	}
	
	function d(){
		var jolin = "jolin"
	}

当然找到了标识符还不够,通常我们还需要取得这个变量的值,这就需要从标识符所在的词法环境所属的执行环境去查找了。

全局环境

在任何 ECMAscript 代码执行之前会创建一个唯一的词法环境作为全局环境。全局环境使用宿主环境的全局对象作为绑定对象,在浏览器中就是window对象。作用域的链的顶端就是全局环境,如果查找到全局环境都找不到变量,程序便会返回undefined。

在非严格模式下的函数代码中,如果使用了没有var 语句创建的变量,该变量会被绑定到全局环境的环境记录项中,在浏览器中就是会被绑定到window对象中。

下面介绍一下执行环境,来了解整个变量查找的过程。

执行环境

ECMA-262规定了3种可执行的代码,全局代码,Eval代码和函数代码。当控制器转入到ECMA脚本的可执行代码时,控制器会进入一个执行环境(execution context)。执行环境一个纯粹的标准机制,在脚本中我们是无法访问到执行环境的,它定义了变量或函数有权访问的其它数据,决定了它们各自的行为。

每个执行环境包含3个状态组件

  • 词法环境组件(LexicalEnvironment)
  • 变量环境组件(VariableEnvironment)
  • this 绑定(ThisBinding)

执行环境栈

当前活动的多个执行环境在逻辑上形成一个栈结构。例如当函数被调用的时候,控制器会进入到一段与原执行环境无关的可执行代码,此时会创建一个新的执行环境,这个新的执行环境会被压入到栈中,因为处在栈顶位置,此执行环境成为当前运行的执行环境。当代码执行完毕后将这个执行环境从堆栈中推出,并将控制权交还给上一个执行环境。

变量的查找

当代码中要查找一个变量时,需要找到当前正在运行的执行环境,也就是执行环境栈栈顶的执行环境。代码需要在这个执行环境的词法环境中查找到这个标识符,如果查不到则会到上一级的词法环境中查找。如果直到全局环境都找不到这个标识符,则会返回undefined。

Example

我们尝试通过对code-2代码进行一步步的解析,来对上面的概念有个相对的理解。

在全局代码执行之前,会初始化全局环境,在浏览器中执行会将window对象作为全局环境的绑定对象。

控制器解析执行全局代码创建并初始化全局环境。初始化全局环境的过程如下,控制器遍历全局代码,碰到一个变量定义和两个函数声明.对于var b = 123会将b的值设为undefined绑定到window对象也就是全局环境对象的绑定对象中。对于fun和baz的函数声明,控制器会根据函数定义的代码创建函数对象,并绑定到绑定对象中。在函数声明代码执行之前,会以当前运行环境的词法环境为外部词法环境引用,创建该函数的词法环境。所以在code-2中,函数fun和函数baz的词法环境的外部词法环境引用都是全局环境。

当代码执行到fun函数执行的时候,控制器开始进入到fun函数中,并且创建一个新的执行环境。新的执行环境将会被压入到执行环境栈中,作为当前运行的执行环境。变量a被添加到该执行环境的绑定对象中,并被赋予值2。

当代码执行到baz函数执行的时候,控制器开始进入到baz函数中,一样会创建一个新的执行环境并压入到执行环境栈中作为当前的运行环境。代码语句console.log(b)执行的时候,需要查找到变量b的值。控制器会先在当前执行环境的词法环境的环境数据项中查找b这个标识符,由于函数内没有定义这个变量,所以无法在当前执行环境的词法环境找到这个变量的引用。此时控制器会从该词法环境的外部词法环境中去查找,由于baz函数声明执行的时候是在全局环境中,所以此时执行环境的词法环境的外部词法环境便是全局环境。控制在全局环境的绑定对象中找到了b标识符,并返回了该标识符的引用给log函数使用。由于变量a没有在全局环境中定义,并且全局环境的外部词法环境为NULL,控制器在查找a这个标识符的最后会抛出一个ReferenceError 异常。

总结

函数声明的执行能创建独立的作用域,它们相互之间的嵌套关系又形成了作用域链,通过这条作用域链能实现对标识符的查找。