JavaScript的执行上下文(Execution Context)是JavaScript的基础,也是一个难点,为什么要学JavaScript的执行上下文呢?执行上下文是个什么东西?执行上下文是为了更清楚理解JavaScript的执行逻辑,有编程基础的应该知道内存空间的栈和堆,这是程序一般的运行基础,而执行上下文也是和内存管理相关,不管如何,理解执行上下文的生命周期能够让你清楚知道JavaScript程序如何运行,更好地分析代码,执行上下文的机制无处不在,像闭包、函数式编程、JS继承、函数声明提升等,掌握JavaScript的核心技术,从理解执行上下文开始。
一、JavaScript内存管理机制
首先我们先了解一下JavaScript的内存管理机制,这种机制和C/C++、Java类似,但是JavaScript并不是真的有完全相同的机制,JavaScript代码由JS引擎解析执行,它的内存管理多少会有些不同,这里取普遍的内存管理机制结合一般的编程风格是没有问题的。
JavaScript的代码最终是在内存中运行的,JS程序的内存其中包括栈内存和堆内存,栈内存的特点是先进后出,由系统分配和回收,运行速度极快,栈类似于装书的箱子,往箱子放一步步书,后面的书叠在上一本上面;堆内存是动态分配的,运行速度相对栈内存稍慢,但是空间分配自由,内存由JS自动回收。
var a = 9;
var b = "string";
console.log(a + b);
var obj = {};
obj.name = "Object";
如上一段简单的代码,a和b是两个基本类型的变量,基本类型的数据一般存储在栈内存中,引用类型的数据存储在堆内存中,obj就是一个引用类型,但是注意,obj本身不保存实际数据,它保存的是对象数据的指针,obj同样存储在栈内存中,而obj指针指向的对象数据则是存储在堆内存中。
程序开始执行,上面的代码从上至下依次进入栈内存,a在栈底,而obj.name在栈顶执行,栈是先进后出、后进先出,所以obj.name执行完毕首先被释放,a则是最后才释放,可以结合下图一起理解。
上图中左边是栈内存,右边是堆内存。总的来说JavaScript代码基本就是入栈执行,然后出栈释放空间。
二、JavaScript执行上下文生命周期详解
执行上下文是基于代码当前的运行环境的,它就是一个对象,不同环境有不同的上下文对象,一般的上下文执行环境有:全局执行环境、函数执行环境和eval执行环境,执行上下文的任务是保存当前环境的相关数据,供其执行使用。JavaScript的执行上下文生命周期整体如下:
在这里我们主要针对典型的函数执行环境分析,全局执行环境类似,eval则不常用。函数在调用时创建执行上下文对象,在这一步会进行一些较为关键的操作,而到入栈执行则比较简单,执行完毕则出栈等待被回收。
1、创建执行上下文
执行上下文是一个对象(Execution
Context Object),创建该对象又可分为三部分:创建变量对象(Variable Object)、创建作用域链(Scope Chain)、确定this指向,如下图:
创建变量对象,变量对象简称VO,VO也是一个对象,创建VO又可按顺序分为:
(1)创建arguments对象:添加一个属性为参数名的变量,并赋值为参数值;
(2)初始化参数:在VO添加名为参数名的变量,赋值为参数值;
(3)添加函数引用:在VO添加名为函数名的变量,赋值为该函数的引用;
(4)添加普通变量:在VO添加名为变量名的变量,赋值为undefined。
对于同名函数或变量,后一个会覆盖掉前一个,如果一个函数和一个变量同名,同名变量会跳过(入栈执行期间一律采取覆盖操作)。
如下为一段简单的代码以及相应的上下文创建例子体现:
function login(message){
var name = "default";
function exit(){
console.log("exit login.");
}
console.log(message);
}
login("everything");
/**
* 创建执行上下文,创建VO,
* ExecutionContext{
* VO:{
* arguments: [object],
* message: "everything",
* exit: [function],
* name: undefined
* },
* ScopeChain: [],
* this: [object]
* }
*/
这里要注意的是,函数是先于变量被声明创建的,仅次于参数,这就是函数声明提升的原因了,另外变量在创建变量对象VO的时候的值为undefined。
创建作用域链,作用域链即关于作用域的链表,这是一个单向链表,什么是JavaScript的作用域?例如函数就是了,JS没有块作用域(for while内部的变量可以在外部访问),只有函数作用域,你可以将函数看作一个作用域,更准确来说,作用域是由变量对象组成的,当前的作用域对象包含当前的变量对象,并指向上一个作用域对象,访问时只能自下而上访问,不能倒过来访问,最后一个作用域对象是全局作用域对象global,global比较特殊,它的VO和this都是window。
上面代码的作用域链如下:
作用域链的作用就是提供标识符访问,例如如果exit函数和login函数同样有一个变量名为email,email在两个执行环境的VO中,在exit函数中访问时,会首先查找exit{}作用域对象的VO中是否有email,若没有则一直往上找。
如果exit函数一直没释放,则login也同样保留在内存中,这个就是闭包的解释了,更改上面代码,下面就是一个闭包的例子:
function login(message){
var name = "default";
console.log(message);
return function exit(){
console.log(name + " exit login.");
}
}
var exit = login("everything");
exit();
确定this指向,全局执行环境默认为window,函数执行环境中,如果是对象调用则为该对象,如果是普通函数调用默认为window,strict下为null。
2、代码入栈执行
此时变量对象VO转为AO激活对象(Active Object),其实就是同一个东西,在这一步中变量对象中的普通未初始化的变量在这里会被赋值执行,包括执行其他代码和函数,在这一步中属于正常的入栈执行操作,结合VO的创建顺序则基本已经能够准备分析JavaScript的执行上下文了。
3、出栈等待回收
JavaScript具有自动垃圾回收机制,程序中的变量在不再使用的时候会被自动回收,JavaScript中的垃圾回收方式是标记清除,当执行上下文入栈执行时变量标记为“进入环境”,出栈标记为“离开环境”,垃圾回收启动的时候会释放这些变量的内存空间。
例如函数执行上下文被创建并被入栈执行,这时函数内部的变量标记为“进入环境”,执行上下文出栈被标记为“离开环境”,里面的变量会被自动回收。
JavaScript的垃圾回收是基于执行上下文的,if语句块,for语句块,while语句块等是没有执行上下文的,所以这些语句块内部定义的变量或函数同样可以在外部访问,离开语句块也不会被释放回收。
若需要手动释放空间,数组可以使用array.length=0情况数组,对象或者变量可以通过设置为null释放内存空间。