js闭包前言
以下内容可能有误,因现在水平不足,待以后修改,但现在够用。
js作为一种解释执行的语言,其执行过程为单线程执行(但是有异步操作,向定时器),且只有两种作用域,全局和局部(还有个eval(),通过eval()执行的代码,具有与该执行环境相同的作用域链,相当于没用eval()直接用代码 )。在执行过程中预设了一个外围的环境全局的VO。当js源码被解释执行时需要经过词法分析(分解成有意义的代码块)、语法分析(语法分析后会生成语法树)的语法检查过程,而后进入预编译(这个阶段完成声明提前)、执行(赋值、执行)的执行过程。在执行过程中,遇到函数的调用,就会到达本文主要记录的部分如下。
在函数的执行时,也就是函数被调用(或者说激活)的时候,会创建一个执行环境(或者叫CE,或者执行上下文)在这个执行环境(注意这里说的是函数的执行环境)中。一共有三个元素组成。作用域链、VO、this。前面说过,js是单线程,并且在js中存在一个执行环境栈,这样在一个时刻只有一个执行环境在起作用。当然,js默认在全局环境(VO)中执行。这样当函数调用的时候,函数的执行环境就被压栈,当函数执行完毕,该执行环境就会被弹出销毁,但其中的AO不一定会被销毁。(函数的变量对象VO和活动对象AO区别不大,所以看到很多都是直接将AO代替VO,就当没有VO)
虽然只是一个函数的调用,但其中还有更为细化的步骤。现需要了解全局对象:进入任何执行环境之前就已经存在的对象。该对象仅一份,以单例形式存在,生命在程序终止时结束。该对象在初始创建阶段将Math、String、Date、parseInt等作为自身属性,其中一些对象会指向全局对象本身——比如,DOM中,全局对象上的window属性就指向了全局对象(当然,并非所有的实现都是如此)。这些属性在js程序的任何地方都可以访问。因为全局对象是不能通过名字直接访问,所以引用全局对象的属性时,前缀通常省略。然而,通过全局对象上的this值,以及通过如DOM中的window对象这样递归引用的方式都可以访问到全局对象,全局执行环境的变量对象(VO)就是全局对象本身。
再说函数的生命周期,以在全局环境下声明的函数为例,并且存在函数的调用,函数的生命周期分为两部分1、创建、2、执行。
创建阶段,这一阶段来自于全局执行环境VO的操作,全局函数声明的提升,全局变量的提升,这两步的提升称为预编译,在进入执行环境时发生,对应着函数的创建阶段(其中函数表达式,不是在预编译的提升阶段创建而是在执行时创建,这种情况个人认为是在创建VO前)。函数在这一阶段创建,并且在函数中会创建一个预先包含全局变量对象的作用域链,这个作用域链存在于函数的[[Scope]]属性中,这个属性即便函数不被调用也会一直存在于函数中。这一阶段存在函数和变量的提升,但是函数的函数体中的变量是不管的,在调用时在函数的局部作用域中提升。
执行阶段,也就是源码执行了,函数被调用了,这时会为函数创建一个执行环境。这里是一个全新的局部的执行环境的创建。该环境又包含两步骤,创建(进入执行上下文阶段)、执行,其创建阶段有三个大步骤会执行。
1、创建VO对象。按照函数参数、函数声明、变量声明的顺序
2、创建作用域链,通过复制函数的[[Scope]]然后再加自身的VO,指向变量对象的指针列表。并不是指向执行对象,要区分。
3、确定this的指向。就算没有执行,从代码也能分析出,所以这里可以确定,比如从代码的语法分析树。
再这三个大步骤的第一步创建VO对象包含的三个小步骤的如下(按如下顺序):
1、函数的所有形参 (如果是函数上下文)
由名称和对应值组成的一个变量对象的属性被创建
没有实参,属性值设为 undefined
2、函数声明
由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
如果变量对象已经存在相同名称的属性,则完全替换这个属性
3、变量声明
由名称和对应值(undefined)组成一个变量对象的属性被创建;
如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
函数提升和变量的提升就发生在创建VO中,函数提升对应着函数的创建(这里其实是在函数内创建函数),在全局环境下也有这个过程。其中不够清晰的是最后一个,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
function test(e, f){ var a = 10; function b(){} var c = function(){}; (function d(){}); //这是函数表达式,因为在分组操作符内(),分组操作符内只能是表达式 //只是一个表达式,也没有赋值给谁。所以VO中没它 alert(x); // function var x = 10; alert(x); // 10 x = 20; function x() {}; alert(x); // 20 } test(e); //在函数调用发生,但源码执行前,VO还没有激活为AO,VO完成时内部如下 VO(test{ e: 10; f: undefined; //没有参数所以为undefined a: undefined; b: <reference to FunctionDeclaration "b"> c: undefined; x: <reference to FunctionDeclaration "x"> //var x 不会干扰已经存在的这类属性,所以没有var x的事,也说明了函数的优先 } //在test(e)内的源码执行时,会执行x=10, x=20。另外一个script作为一个解析单元
在函数的执行环境的创建阶段的三大步骤完成,创建阶段也就完成了,然后就是执行阶段,(注意这里强调时函数的执行环境,因为全局执行环境的VO是可以间接访问的,且不存在AO),在函数的局部执行环境中创建的VO是给js解释器看的不能直接访问,但是却需要得到VO中的值,因为这些都是我们定义的,必须要能够访问。所以为了能够访问其中的属性,这时会创建一个AO对象(或者说将VO激活为AO),与该执行环境相关联,该对象不可访问,只能访问其成员。AO = VO + function parameters + arguments。多了函数执行时的实参和arguments。arguments的属性值是Arguments对象。Arguments对象是活跃对象上的属性,它包含了如下属性:callee —— 对当前函数的引用,length —— 实参的个数,properties-indexes(数字,转换成l字符串)其值是函数参数的值(参数列表中,从左到右)。properties-indexes的个数 == arguments.length;arguments对象的properties-indexes的值和当前(实际传递的)形参是共享的。个人认为应该是在执行环境创建完成后,执行之前进行的压栈。
在解析的过程中一个<script></script>中的内容作为一次解释执行的单元,也就是先解释执行一个单元,在解释下一个,按顺序来。

浙公网安备 33010602011771号