深入剖析函数执行流程
本篇文章,对JavaScript函数的执行流程做了细致入微的分析。话不多说,我以下面这个例子作为样例——
1 (function ($){ 2 3 function foo() { 4 var x = 10; 5 return function bar() { 6 console.log(x); 7 }; 8 } 9 10 var f = foo(); 11 12 var x = 20; 13 14 f(); // 结果是10而不是20 15 16 })(jQuery)
对于它的执行过程,可用一张图来表示,如下——
函数执行流程图解
对于该图的说明如下——
1 /** 2 * 首先需要明白函数执行流控制机制: 3 * 1>进入函数具有两个步骤: 4 * a>进入函数执行环境 5 * 注意:在进入执行环境之前,会创建有关该执行环境的活动对象对象AO,主要包括三个内容—— 6 * i>函数参数 7 * ii>该执行环境内的所有函数声明 8 * iii>该执行环境内的所有变量声明 9 * b>执行代码 10 * 注意:只有在代码执行的时候才对声明了的变量进行赋值操作。 11 * 2>每个函数在被调用时都会创建自己的执行环境,接着将函数的执行环境推入环境栈中。 12 * 在函数执行完之后,将控制权返回给之前的执行环境。是典型的堆栈控制方式。 13 * 3>从这个角度来讲,一旦刚开始执行JS代码,就可以理解为调用了这个全局函数。紧接着,做了一个调用普通函数该做的事情。 14 */ 15 16 /** 17 * 这个匿名函数是第一个匿名函数,在这里称之为Anonymous Function 1,简称AF1 18 * 当AF1被调用时,会出现3个过程: 19 * 1>创建该函数,创建该函数的包含全局变量对象的[[scope]]属性; 20 * 2>调用函数后,创建AF1执行环境,并压入执行环境堆栈中,此时的ECStack = [ AF1Context, globalContext ]; 21 * 此时的执行环境包括3个内容: 22 * a>this指针:Window;需要注意的是,this指针是包含在变量(活动)对象里的。 23 * b>作用域链Scope:这里的Scope = AF1.Context.Ao + AF1.[[scope]] 24 * = AF1.Context.Ao + globalContext.VO; 25 * 注意:这里的作用域链本质上是一个指向变量对象的列表,它只引用,但不包含变量对象。 26 * 这里的[[scope]]属性是所有父级变量对象的层级链,对于下面的[[scope]]属性皆为如此。 27 * 对于变量对象和活动对象,在具体实现层面上只是一个抽象概念。 28 * c>活动对象AO:这里的AO = { 29 * foo: <reference to function>, 30 * f : undefined, 31 * x : undefined 32 * } 33 * 注意:这里的f和x值是在执行函数的时候赋值的。 34 * 3>接着就是执行AF1内部代码了。 35 */ 36 (function ($){ 37 38 function foo() { 39 /** 40 * 1>创建foo函数的执行环境,并压入执行环境栈中,此时的ECStack = [ fooContext, AF1Context, globalContext ]; 41 * 2>这个函数的作用域链scope = [ fooContext.AO + AF1Context.Ao + globalContext.VO] 42 */ 43 var x = 10; //这句执行完成后,这个x的值变为10,而且这个变量x是属于fooContext.AO的。 44 45 //这里的匿名函数是第二个匿名函数,在这里在称之为Anonymous Function 2,简称AF2 46 return function() { 47 /** 48 * 1>创建foo函数的执行环境,并压入执行环境栈中,此时的ECStack = [ AF2Context, AF1Context, globalContext ]; 49 * 2>这个函数的作用域链scope = [ fooContext.AO + AF1Context.AO + globalContext.VO ] 50 * 3>当程序开始检索x变量时,根据作用域链的先后顺序开始查找,即先去fooContext.AO中查找, 51 * 如果没有,就从AF1Context.AO中查找,发现找到了x变量,就停止对剩余的变量对象VO中的x变量的查找过程。 52 * 自然就能够清晰的明白,输出结果为什么是10,而不是20。从而就能够明白静态作用域和动态作用域的区别了。 53 */ 54 console.log(x); 55 }; //当这个匿名函数返回后,此时的ECStack = [ AF1Context, globalContext ]; 56 } //foo函数返回后,fooEC退出栈顶,此时的ECStack = [ AF1Context, globalContext ]; 57 58 /** 59 * 这种函数调用模式被称作函数调用模式(另外还包括方法调用模式、构造器调用模式和Apply调用模式) 60 * 当函数以此模式调用时,this被绑定到全局对象。对于this指针,作以如下说明: 61 * 1>它是执行环境的一个属性,它以活动对象的其中一员进行呈现; 62 * 2>它是在进入执行环境的时候被确认,并且在执行环境运行期间永久不变; 63 * 3>它并没有类似变量向上一层一层搜索的过程,直接从执行环境中获取; 64 * 4>它只能获取,不能赋值。 65 * 5>它是由激活对应函数执行环境的调用者来提供的,即调用函数的父执行环境; 66 * 调用函数的方式影响了对应函数执行环境的this值。 67 * 执行完这句代码,开始调用foo函数。 68 */ 69 var f = foo(); 70 71 var x = 20; //执行完x = 20;这句代码后,也就说明AF1的活动对象的赋值就完成了。 72 73 f(); // 开始调用AF2 74 75 })(jQuery) //当这个函数返回后,此时的ECStack = [ globalContext ]; 76 77 /** 78 * 从这个例子可以看出,执行环境栈ECStack是如何工作的,ECStack一直保存着全局执行环境globalContext 79 * 对于全局执行环境,以下做以几点说明—— 80 * 1>当碰见可执行代码时,便进入执行环境,全局执行环境是最外围执行环境 81 * 2>何时消亡:退出应用程序——关闭网页或浏览器 82 * 3>活动的执行环境组逻辑上组成一个堆栈,堆栈底部永远是全局执行环境 83 * 4>在进入全局执行环境之前,会创建全局变量对象,这个对象只存在一份,它的属性在任何地方都可以访问 84 */ 85 86 /** 87 * ----------------------------------------- 88 * 参考资料: 89 * 汤姆大叔博客—— 90 * JavaScript核心 91 * 执行上下文 92 * 变量对象 93 * this指针 94 * 作用域链 95 * 函数(Functions) 96 * 闭包(Closures) 97 * JavaScript高级程序设计(第二版)—— 98 * 第四章:变量、作用域和内存问题 99 * 第七章:匿名函数 100 * JavaScript语言精粹—— 101 * 第四章:函数 102 * ----------------------------------------- 103 * 104 */
理解这个例子的执行流程,相信就会深入的理解下面几个关键词(概念)——
1>静态作用域、动态作用域
2>闭包——共享作用域和私有作用域
3>标识符解析
4>变量对象、活动对象
5>函数生命周期
举一反三,任何JavaScript代码的执行,均能做以细致分析,每执行一句代码,我们就能知道下面几点要素——
1>变量或函数是否已赋值
2>哪些变量或函数已经被销毁,哪些依旧存在于内存中
3>当前执行环境的this指针是什么
4>当前执行环境能够访问到哪些变量
知道了函数的执行流程,再去深入理解闭包和模块模式,就会变得非常清晰了。