在弄明白什么是词法环境(Lexical Environments)、什么是运行上下文(Execution Context)、函数是被创建的过程以后,我们要来理解javascript的作用域就非常的容易。
我们知道在JavaScript语言中,有两种类型的作用域:
- 全局作用域
- 函数作用域
那这两种作用域是如何连接起来的呢?
其实我们前面已经提到很多次,复习一下我们的运行环境模型:
Runtime = {
executionContextStack: [];
};
//获取当前的运行上下文,也就是运行栈,栈顶的运行上下文
Runtime.getRunningExecutionContext = function() {
return this.executionContextStack[this.executionContextStack.length - 1];
}
//把运行栈站定的运行上下文弹出
Runtime.pop = function() {
this.executionContextStack.pop();
}
//把一个运行上下文放到运行栈的栈顶
Runtime.push = function(newContext) {
this.executionContextStack.push(newContext);
}
//在当前运行上下文登记一个变量信息
Runtime.register = function(name) {
var env = this.getRunningExecutionContext().VariableEnvironment;
env.EnvironmentRecord.register(name);
}
//在当前运行上下文初始化一个变量信息
Runtime.initialize = function(name,value) {
var env = this.getRunningExecutionContext().VariableEnvironment;
env.EnvironmentRecord.initialize(name,value);
}
//在当前运行上下文上,解析一个标识符
Runtime.getIdentifierVaule = function (name) {
var env = this.getRunningExecutionContext().LexicalEnvironment;
while(env){
var envRec = env.EnvironmentRecord;
var exists = envRec.isExist(name);
if(exists) return envRec.getValue(name);
env = env.outer;
}
}
复制代码
我们解析一个变量,就是在当前运行运行上下文(Execution Context)上查找是否存在这个变量。那运行上下文由什么构成?
由词法环境(Lexical Environments)环境构成:
function ExecutionContext() {
this.LexicalEnvironment = undefined;
this.VariableEnvironment = undefined;
this.ThisBinding = undefined;
}
function LexicalEnvironment() {
this.EnvironmentRecord = undefined;
this.outer = undefined; //outer Environment Reference
}
复制代码
词法环境(Lexical Environments)又是负责登记本环境(function内的)变量的。函数内的变量登记在当前词法环境(Lexical Environments)的EnvironmentRecord里面,函数如何访问到全局的变量的,这些变量并没登记在函数词法环境上?
通过LexicalEnvironment的outer。
函数运行时的过LexicalEnvironment的指向的是函数对象的[[scope]],而函数对象的[[scope]]保存着函数创建时的词法环境。
因此函数中,可访问词法环境范围就是:函数自己的词法环境+函数创建时的词法环境。如果函数创建时的词法环境也是一个函数的运行环境(内嵌函数),函数的可访问的词法环境就变成:
函数自己的词法环境+函数创建时的词法环境 = 函数自己的词法环境+外面函数的词法环境+外面函数创建时的词法环境。。。
复制代码
就这样一个个的函数词法环境链接在一起,就是就是函数的作用域链。
我们分析一段代码更清晰:
var bestAvenger = "global";
function a() {
var bestActor = "in a";
console.log(bestAvenger); // "global";
function c() {
var bestC = "in c";
console.log(bestActor); // "in a";
b();
}
c();
}
function b() {
console.log(bestC); //**not defined error**
}
a();
复制代码
-
全局运行上下文初始化:
//创建全局运行上下文 var globalExecutionContext = new ExecutionContext(); globalExecutionContext.LexicalEnvironment = creatGlobalEnvironment(globalobject); globalExecutionContext.VariableEnvironment = creatGlobalEnvironment(globalobject); globalExecutionContext.ThisBinding = globalobject; //入栈 Runtime.push(globalExecutionContext); //这时的Runtime = { // executionContextStack: [globalExecutionContext] //} 复制代码
看起来是这样的:
-
开始登记各个声明
-
执行代码
- bestAvenger = "global";
- 遇到a的调用,a()。以a的函数对象的[[scope]]创建a的运行上下文,
模型伪代码如下:
//创建新的运行上下文 var barExecutionContext = new ExecutionContext(); //创建一个新的词法环境(Lexical Enviroment) var localEnviroment = new LexicalEnvironment(); //创建新的EnvironmentRecord var barEnvironmentRecord = new EnvironmentRecord(); localEnviroment.EnvironmentRecord = barEnvironmentRecord localEnviroment.outer = [[scope]] of bar function object barExecutionContext.LexicalEnvironment = localEnviroment; barExecutionContext.VariableEnvironment = localEnviroment; barExecutionContext.ThisBinding = globalobject;//此例子中thisArg是undefined,且不是strict,所以设置为 globalobject //把函数的运行上下文入栈: Runtime.push(barExecutionContext); 复制代码
这时候运行栈:
-
开始在函数a的运行上下文上登记a函数内的声明:
- 变量bestActor
- 函数声明c
-
开始执行a函数里面的语句:
- bestActor = "in a";
- console.log(bestAvenger); //子当当前运行上下文查找bestAvenger,途径就是图中绿虚线的途径:
- 执行函数c调用,同样的创建运行上下文,入栈登记变量,执行语句
- bestC = "in c";
- console.log(bestActor); // "in a"; bestActor的查找入境如图中蓝色虚线所示
- 接着运行函数b调用,同一样,创建运行上下文入栈,
大家会发现在当前运行上下文上,找不到变量bestC。
所以,作用域并不是按照运行上下文,上一个链接下一个,这样链接起来的。而是按照代码的词法结构,文本结构,链接起来的。
在上面的例子中:a,c是在全局运行山下文上创建的,所以a、c的作用域链是,本身函数作用域链接全局作用域。
所以尽管在函数c里运行b函数,b函数作用域链上并未不能找到在c中定义的变量。
思考
看一段代码片段:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); //3
data[1](); //3
data[2](); //3
复制代码
你能用我们提到过的运行模型分析这段代码,为什么三个函数调用都是打印3吗?