上次一篇简单的干货分享,只想让你分分钟学会javascript中的闭包,这就好比一辆车摆在你面前让你束手无策,我只想让你简单的学会驾驶它,仅此而已。而这次我们将从原理层面分析 javascript 引擎如何执行一段代码、一个函数,以及如何形成一个闭包,让你从“爱的初体验” 变得 “深爱” 。
1 var a = 1; 2 3 function f1(b){ 4 var a = 2; 5 function f2(c){ 6 console.log(a+b+c); 7 } 8 return f2; 9 } 10 var fn = f1(1); 11 fn(1); >>> 4
这是一个很简单的javascript闭包,执行结果是4.
下面我们将从 全局初始化、执行f1、执行fn 三个阶段来分析这段代码的执行过程:
JS引擎在进入一段可执行的代码时,需要完成以下3步初始化工作:
(1)创建一个全局对象(Global Object) , 这个对象全局只存在一份,它的属性在任何地方都可以访问,它的存在伴随着应用程序的整个生命周期,全局对象在创建时,将Math,String,Date,document等常用的JS对象作为其属性,由于这个全局对象不能通过名字直接访问,因此还添加了另外一个属性window,并将window指向了自身,这样就可以通过window访问这个全局对象了,如图所示:
(2)构建一个执行环境栈( Execution Context Stack) , 创建一个全局执行环境(Execument Context)EC ,并将EC压入栈中。 执行环境栈的作用是为了保证程序能够按照正确的顺序被执行,每个函数都有自己的执行环境,当执行进入一个函数时,函数的执行环境就会被推入一个执行环境栈的顶部并获取执行权。 当这个函数执行完毕,它的执行环境又从这个栈的顶部被删除,并把执行权并还给之前执行环境。如图所示:
(注:本段引用 http://www.cnblogs.com/pigtail/archive/2012/07/19/2570988.html )
本例中,在执行环境栈中只有全局执行环境。
(3)创建一个全局变量对象(Varibale Object) VO, 并将VO指向全局对象,VO中不仅包含了全局对象的属性,还包括在全局定义的变量 a 和函数 f1,于此同时,在定义f1的时候,为 f1 添加了一个内部属性[[scope]],并指向了VO。
[[scope]] 是函数的作用域, 每个函数在定义的时候,都会创建一个与之相关的作用域,函数的作用域总是指向定义函数时所在的环境 ,即f1的作用域指向了VO。 如图所示:
当执行进入f1(1) 时,JS引擎需要完成以下4 步操作:
(1)创建 f1 的执行环境EC,然后EC推入执行环境栈的顶部并获取执行权。 此时执行环境栈中有两个执行环境,分别是全局执行环境和f1执行环境,f1执行环境在栈顶,全局执行环境在栈的底部。
(2)创建一个作用域链(Scope Chain) ,每个执行环境都有自己的作用域链,用于标识符解析,当执行环境被创建时,它的作用域链初始化为当前运行函数的[[scope]]所包含的对象,即全局对象,如图所示:
(3)创建一个函数的活动对象(Activation Object) AO, 函数中的活动对象就是前文中提到的变量对象,只是命名不同而已(你可以认为,变量对象是一个抽象概念,活动对象是它的一个具体实现), 该对象包含了函数的形参、arguments、this、以及局部变量和内部函数的定义,然后此对象会被推入作用域链的前端。如图所示:
(4)在定义f2的时候,同样也为f2添加了一个[[scope]],并指向了定义f2时所在的环境AO<f1>, 而AO<f1> 位于链表的前端,由于链表具有首尾相连的特点,因此f2的作用域指向了f1的整个作用域链,如图所示:
f1被执行以后,返回了f2的引用,并赋值给了fn,执行 fn(1) 就相当于执行f2(1), JS引擎需要完成以下4步操作:
(1)创建 f2 的执行环境EC,然后EC推入执行环境栈的顶部并获取执行权。 此时执行环境栈中有两个执行环境,分别是全局执行环境和f2执行环境,f2 执行环境在栈顶,全局执行环境在栈的底部。
(2)创建f2的作用域链,初始化为 f2 的[[scope]]所包含的对象,即f1的作用域链。
(3)创建f2的活动对象,该对象包含了f2的形参c, arguments 和 this,如图所示:
(4) 执行 a+b+c, f2 在解析a、b、c三个标识符时,遵守变量查找规则:先查找自身是否存在该变量,如果存在,则停止查找;如果不存在,会沿着作用域链依次查找,直到找到为止,如果整个原型链上都未找到该变量,则返回undefined。
话说f2的作用域链到底是什么样呢? 根据过程(3)的示意图,可以得出 f2 的作用域链示意图:
如图所示:变量 c 将在AO<f2> 中被找到,值1; 变量b和a 都会在AO<f1>中被找到,其值分别为1和2,此时停止查找,返回并进行计算,因此输出结果为:1+1+2=4
以上便是JS引擎定义和执行一个闭包的全过程,从 f2 形成的作用域链可以看出,f2的执行依赖于 f1 的AO和全局VO,而f1的AO中保存着f1中的所有变量,从而 f2 间接的引用了f1中的变量。因此,为了保证f2的正确执行,f1 中的变量们 “不得不” 停留在内存中,为 f2 提供“服务”,这样就形成了闭包。
finish!