JavaScript中的闭包
在介绍JavaScript的闭包前,首先需要搞清楚以下几个概念:
闭包:
计算机科学中的闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量(未绑定到特定对象)的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
以上是维基百科中对于闭包的定义,后面会详细解释JavaScript中是如何实现闭包的,先让我们看一个JavaScript中最简单的闭包:
var scope = "global scope "; function checkscope() { var i = 0; var scope = "local scope "; function f() {return scope + (i++);} return f; } var testF = checkscope(); console.log(testF()); // 输出local scope 0 虽然在全局空间中执行函数,但是scope变量的值还是函数内部定义的值 console.log(testF()); // 输出local scope 1 这里的i变量处于checkscope的作用域中,如果没有闭包的存在,在checkscope执行完成后,也会随即被销毁,但是由于testF引用了f(),在testF没有被销毁前,i的值能一直被保留。 console.log(checkscope()()); // 输出local scope 0 console.log(checkscope()()); // 输出local scope 0
第一类对象:
第一类对象(First-class object,第一类公民)也是计算机科学中的一个术语,指可以在执行期创造并作为参数传递给其他函数或存入一个变量的实体。一般具有以下特性:
- 可以被存入变量或其他结构
- 可以被作为参数传递给其他函数
- 可以被作为函数的返回值
- 可以在执行期创造,而无需完全在设计期全部写出
- 即使没有被系结至某一名称,也可以存在
从这些特性我们可以知道JavaScript中的函数就是属于第一类对象,你可以像使用原始值(字符串,数字)一样使用它们。
作用域:
和大多数语言一样,JavaScript采用的是静态作用域,也被称为词法作用域。词法变量的作用域可以是一个函数或一段代码,该变量在这段代码区域内可见(visibility);在这段区域以外该变量不可见(或无法访问)。词法作用域里,取变量的值时,会检查函数定义时的文本环境,捕捉函数定义时对该变量的绑定。不过JavaScript只支持函数作用域(function scope),不支持块级作用域(block scope),例如
function test(o) { var i = 0; // i 在整个函数体内均有定义 if(typeof o == "object") { var j = 0; // j 在函数体内也均有定义,而不仅仅是这个if代码段内 for(var k = 0; k < 10; k++) { // k 在函数体内也均有定义,而不仅仅是for循环内 console.log(k); // 输出数字0~9 } console.log(k); // 输出10, k 已经在循环体内定义了,而且在循环结束后为赋值为10 } console.log(j); // j已经被定义了,但可能没有初始化 }
执行上下文和作用域链:
JavaScript中存在一个比较特殊的对象--全局对象(Global Object)。当JavaScript解释器启动时(或者任何Web浏览器加载新页面的时候),会创建一个新的全局对象,并且会初始化一些初始属性,这些初始属性在任何地方都可以直接使用,下面就是其中的一些初始属性
- 全局属性:比如undefined, Infinity, NaN等
- 全局函数:比如isNan(), parseInt(),eval()等
- 内置构造函数:比如Date(), RegExp(), String()等
- 全局对象:比如Math,JSON等
并且我们自定义的全局变量、函数,都会被当成全局对象的属性来看待。
我们都知道在JavaScript中,全局变量在程序中始终是有定义的,而局部变量在声明它的函数体内以及嵌套的函数体内始终是有定义的。JavaScript是如何实现这一特性的呢?
JavaScript是一门单线程语言,这意味着解释器在一个时间点只能做一件事。当JavaScript解释器初始化执行代码时,它首先默认进入一个全局执行上下文。在此基础上每一次函数的调用都将创建一个新的执行上下文。
创建一个新的执行上下文包括两个阶段:
- 创建阶段【当函数被调用,但未执行任何其内部代码之前】
- 创建作用域链(Scope Chain)
- 创建变量,函数和参数。
- 求”this“的值。
- 激活/代码执行阶段:
- 指派变量的值和函数的引用,解释/执行代码。
所以可以将执行上下文抽象成如下的一个对象:
JavaScript中一切都是对象,函数也是对象,也具有自己的属性,方法。不论是通过函数定义表达式,还是语句,其实都定义了一个函数对象,并且为这个函数对象初始化了一个名为scope chain的属性,引用着一个被称为作用域链的对象,这个属性是一个内部实现,我们通过下面的例子来解释具体的过程:
var x = "x in global"; // 全局变量 x 顶层代码中,作用域链只包含一个对象,也就是全局对象 function foo() { // 全局函数 foo() 定义了一个函数对象,此时这个函数对象的作用域链包含了两个对象,第一个是包含函数参数和局部变量的对象,第二个是创建函数对象的作用域链,在此作用域链中只有一个全局对象 var x = "x in foo"; // 局部变量 x function bar() { // 嵌套函数 bar() 定义了一个嵌套函数对象,此时这个函数对象的作用域链包含了3个对象,第一个是包含函数参数和局部变量的对象,第二个是创建函数对象的作用域链,也就是foo的作用域链, // foo中原来的作用域链中有2个对象,所以这里就有3个对象 var x = "x in bar"; // 嵌套函数内部的局部变量 x var y = "y in bar"; // 嵌套函数内部的局部变量 y } bar(); } foo();