上一篇咱们了解到了函数在不一样状况下是如何被建立的,如今咱们来探讨当函数被调用后作了什么?javascript
回忆一下第二章:java
总结上述,过程咱们构建一个JS的运行模型,进入可执行代码,都会走这个运行模型:浏览器
可运行代码(Executable Code)
ECMAScript 5 规范,定义了三类可运行代码(Executable Code) ,运行这些代码时候会建立运行上下文(Execution Contexts):bash
- global code:就是js整个“程序”,就是源代码文件中全部不在function体中的代码。
- function code:就是函数体中的代码,除了内嵌函数体中的代码之外
- eval code : 就是传给内置eval函数的代码字符串
运行模型
运行代码 = 运行上下文初始化 + var声明和函数声明扫描scan + 执行语句; 复制代码
同时咱们也构建运行环境模型:app
/**
* 运行环境模型伪代码
*/
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.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.outerEnvironmentReference;
}
}
function ExecutionContext() {
this.LexicalEnvironment = undefined;
this.VariableEnvironment = undefined;
this.ThisBinding = undefined;
}
function LexicalEnvironment() {
this.EnvironmentRecord = undefined;
this.outerEnvironmentReference = undefined;
}
function EnvironmentRecord(obj) {
if(isObject(obj)) {
this.bindings = object;
this.type = 'Object';
}
this.bindings = new Map();
this.type = 'Declarative';
}
EnvironmentRecord.prototype.register = function(name) {
if (this.type === 'Declarative')
this.bindings.set(name,undefined)
this.bindings[name] = undefined;
}
EnvironmentRecord.prototype.initialize = function(name,value) {
if (this.type === 'Declarative')
this.bindings.set(name,value);
this.bindings[name] = value;
}
EnvironmentRecord.prototype.getValue = function(name) {
if (this.type === 'Declarative')
return this.bindings.get(name);
return this.bindings[name];
}
function creatGlobalEnvironment(globalobject) {
var globalEnvironment = new LexicalEnvironment();
globalEnvironment.outer = null
globalEnvironment.EnvironmentRecord = new EnvironmentRecord(globalobject)
return globalEnvironment;
}
GlobalEnvironment = creatGlobalEnvironment(globalobject)//能够看做是浏览器环境下的window
复制代码
函数调用分为几类:异步
这几种调用方式,有什么不一样呢?其实在真正进入函数代码运行以后是同样的,这几种调用方式的不一样是在准备进入函数代码运行以前作的准备不同。函数
就像你们去影院看电影,在进入影厅以前,有的同窗买爆米花,有的同窗买汉堡,有点同窗买瓶奶茶,带在身上,进入影厅之后你们的流程就相同了,找排号,找座位,坐下。。。。oop
有没有发现,在影厅里里面,你们在同一个环境,可是每一个人带的"食品"不同。这个影厅里的"食品",就是函数里的"this"。ui
那这五种调用方式,在进入函数代码运行以前,携带进去的,要做为this的"东西"都是啥呢?this
进入函数代码之后呢,和global过程相似:
运行模型
运行代码 = 运行上下文初始化 + var声明和函数声明扫描scan + 执行语句; 复制代码
仍是这三步。
咱们经过分析上一篇开头的代码来讲明其过程
var a = 2;
function foo() {
console.log(a)
}
function bar(){
var a = 5;
foo()
}
bar()//2
复制代码
咱们经过分析函数调用过程,来看看,为何foo() 引用的是全局的a而不是bar里的a。
//建立全局运行上下文
var globalExecutionContext = new ExecutionContext();
globalExecutionContext.LexicalEnvironment = creatGlobalEnvironment(globalobject);
globalExecutionContext.VariableEnvironment = creatGlobalEnvironment(globalobject);
globalExecutionContext.ThisBinding = globalobject;
//入栈
Runtime.push(globalExecutionContext);
//这时的Runtime = {
// executionContextStack: [globalExecutionContext]
//}
复制代码
看起来是这样的:
扫描var 声明:“var a = 2;”
var currentEnvironment = Runtime.getRunningExecutionContext().VariableEnvironment;
currentEnvironment.EnvironmentRecord.register('a');
复制代码
扫描到函数声明:“function foo() {console.log(a)}”
//获取当前运行上下文的词法环境
var currentEnvironment = Runtime.getRunningExecutionContext().VariableEnvironment;
//建立函数
var fo = FunctionCreate([],"console.log(a)",currentEnvironment,false)//详细过程看上一篇
currentEnvironment.EnvironmentRecord.initialize('foo',fo);
复制代码
扫描到函数声明:"function bar(){ var a = 5;foo()}"
//获取当前运行上下文的词法环境
var currentEnvironment = Runtime.getRunningExecutionContext().VariableEnvironment;
//建立函数
var fo = FunctionCreate([]," var a = 5;foo()",currentEnvironment,false)//详细过程看上一篇
currentEnvironment.EnvironmentRecord.initialize('bar',fo);
复制代码
这时候整个环境看起来是这样的:
执行语句
var currentEnvironment = Runtime.getRunningExecutionContext().LexicalEnvironment;
currentEnvironment.EnvironmentRecord.initialize('a',2);
复制代码
执行调用语句:bar()
bar()运行之后,上述讲到,会携带undefined做为thisArg,开始进入函数代码的运行。
函数代码的执行和global的执行相似,也遵循咱们的运行模型:
运行模型
运行代码 = 运行上下文初始化 + var声明和函数声明扫描scan + 执行语句; 复制代码
初始化函数的运行上下:
模型伪代码以下:
//建立新的运行上下文
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);
复制代码
这时整个环境看起来是这样的:
整个过程简化来来是说:用函数自身建立时候携带的词法环境为“父”,建立一个函数本身的词法环境。
图中虚线的意思,就是outer的实际的指向。函数运行时候的词法环境的outer指向了函数建立时的词法环境。而咱们知道bar函数在全局运行上下文上建立的,建立时的词法环境为全局词法环境(GlobalEnvironment)。所以outer实际是指向全局词法环境。
因此这里你应该清楚了,函数运行时的词法环境由两部分组成:“先天” + “后天”,先天就是函数建立时的词法环境,后天就是运行时新建立的词法环境,两个链在一块:
我为何一直强调"函数建立时的词法环境",由于这个很重要:就是函数运行时的词法环境和它被调用时那一刹那的词法环境无关,而只与它被建立时的词法环境相关。
好了,bar的运行上下文建立完了,接着开始扫码函数里的代码。
var声明和函数声明扫描scan:
//注意:这次在栈顶的是bar的运行上下文
//因此getRunningExecutionContext().LexicalEnvironment返回的是bar函数的词法环境
var currentEnvironment = Runtime.getRunningExecutionContext().LexicalEnvironment;
currentEnvironment.EnvironmentRecord.initialize('a',2);
复制代码
这时图上看是这样的:
bar里面只有一个声明,接着执行语句。
执行语句
执行语句:a = 5;
执行函数调用foo(): 和执行bar过程相似,再也不赘述,建立一个新的运行上下文,并进入栈顶
从图中,咱们看一看出,foo运行时的词法环境和foo刚刚被调用那时刻的词法环境不要紧。只和它建立时的词法环境相关。
当foo中执行语句:“console.log(a)”时候,会去当前的词法环境查找a,图中能够看出,当前词法环境是空的,所以就找当前词法环境的outer---也就是函数建立时的词法环境(保存在函数内部属性[[scope]]中),也就是全局词法环境,找到了a:2,所以打印2。
函数运行完毕的返回值,分两种状况:
返回后,把函数的运行上下文出栈。
```
//foo()运行完毕,回到bar的运行上下文
Runtime.pop();
//bar运行完毕,回到global 运行上下文
Runtime.pop();
//global 运行上下文 已经无其余语句,弹出global全局上下文
Runtime.pop();
```
复制代码
最终把运行栈清空:
看图中的对象结构,已经没代码引用它们。它们孤零零的存在内存中,后续就会被js引擎的垃圾回收机制给清除。
从上面的分析,咱们知道函数的运行时的环境和函数建立时候的环境紧密相连,而和函数被调用时的环境不要紧。这就是静态词法环境的意思(可认为就是静态做用域,由于还没谈到做用域的概念,因此用此法环境的说法)。
上篇咱们提到的一种特殊状况,那就是new Function()方式建立的函数,这种方法建立的函数,函数对象中的[[scope]],永远是global词法环境。因此,无论new Function()在什么样的的环境中建立函数,其函数运行时的都是全局环境+本身函数内部的词法环境。
这就是这段代码中innerTwo()会输出1的缘由:
var a = 1;
function foo() {
var a = 2;
function innerOne(){
console.log(a)
}
var innerTwo = new Function("console.log(a)")
var innerTree = function (){
console.log(a)
}
innerOne();
innerTwo();
innerTree();
}
foo();//2 1 2
复制代码
以前笔者看帖子有小伙伴提到:
只有当整个应用程序结束的时候,ECStack 才会被清空,因此程序结束以前, ECStack >最底部永远有个 globalContext:
这时有个小伙伴针对这句话提问:
ECStack能够理解为执行栈,可是JS在处理定时器、DOM事件监听等异步事件时,会将其放入Event Table,知足触发条件后会发送到消息队列,这时候只有检测到调用栈为空的时候,才会把队列中事件放到栈中执行。你这里的意思是在整个执行过程当中globalContext是一直存在的吗?那这里的矛盾应该如何解释,求教,谢谢。
意思就是既然说全局运行栈在栈底,并且程序结束的时候,全局运行栈也会被清空,并且只有运行栈为空了,事件函数才能入栈,那这时 globalContext 都不见了,事件函数里面是怎么找到全局变量的呢?
结合本篇和上篇函数的建立和调用过程,你能回答这个问题吗?
请试着解释以下代码:
var onGlobal = 'on Global ';
setTimeout(function(){
console.log(onGlobal)
},1000);
复制代码
setTimeout的回调,只有在运行栈为空时(后续咱们聊到event loop 会谈到这个),才会被推入运行栈,那这时候全局运行栈不在了,回调函数如何找到onGlobal这个变量的值的呢?