前面介紹了作用域鍊和變數對象,現在再講閉包就容易理解了。閉包其實大家都已經談爛了。儘管如此,這裡還是要試著從理論角度來討論下閉包,看看ECMAScript中的閉包內部究竟是如何運作的。
在直接討論ECMAScript閉包之前,還是必須先來看看函數式程式設計中一些基本定義。
眾所周知,在函數式語言中(ECMAScript也支援這種風格),函數就是資料。就比方說,函數可以賦值給變量,可以當參數傳遞給其他函數,還可以從函數傳回等等。這類函數有特殊的名字和結構。
定義
A functional argument (“Funarg”) — is an argument which value is a function.
函數式參數(“Funarg”) —— 是指值為函數的參數。
範例:
function exampleFunc(funArg) { funArg(); } exampleFunc(function () { alert('funArg'); });
上述例子中funarg的實際參數其實是傳遞給exampleFunc的匿名函數。
反過來,接受函數式參數的函數稱為高階函數(high-order function 簡稱:HOF)。也可以稱作:函數式函數或偏數理或操作符。上述例子中,exampleFunc 就是這樣的函數。
先前提到的,函數不僅可以作為參數,還可以作為回傳值。這類以函數為傳回值的函數稱為帶函數值的函數(functions with functional value or function valued functions)。
(function functionValued() { return function () { alert('returned function is called'); }; })()();
可以以正常資料形式存在的函數(比方說:當參數傳遞,接受函數式參數或以函數值返回)都稱作 第一類函數(一般說第一類物件)。在ECMAScript中,所有的函數都是第一類物件。
函數可以作為正常資料存在(例如:當參數傳遞,接受函數式參數或以函數值返回)都稱作第一類函數(一般說第一類物件)。
在ECMAScript中,所有的函數都是第一類物件。
接受自己作為參數的函數,稱為自應用函數(auto-applicative function 或self-applicative function):
(function selfApplicative(funArg) { if (funArg && funArg === selfApplicative) { alert('self-applicative'); return; } selfApplicative(selfApplicative); })();
以自己為傳回值的函數稱為自複製函數(auto-replicative function 或self-replicative function)。通常,「自複製」這個詞用在文學作品中:
(function selfReplicative() { return selfReplicative; })();
自複製函數的其中一個比較有意思的模式是讓僅接受集合的一個項作為參數來接受從而代替接受集合本身。
// 接受集合的函数 function registerModes(modes) { modes.forEach(registerMode, modes); } // 用法 registerModes(['roster', 'accounts', 'groups']); // 自复制函数的声明 function modes(mode) { registerMode(mode); // 注册一个mode return modes; // 返回函数自身 } // 用法,modes链式调用 modes('roster')('accounts')('groups') //有点类似:jQueryObject.addClass("a").toggle().removClass("b")
但直接傳集合用起來相對來說,比較有效且直觀。
在函數式參數中定義的變量,在「funarg」啟動時就能夠存取了(因為儲存上下文資料的變數物件每次在進入上下文的時候就創建出來了):
function testFn(funArg) { // funarg激活时, 局部变量localVar可以访问了 funArg(10); // 20 funArg(20); // 30 } testFn(function (arg) { var localVar = 10; alert(arg + localVar); });
然而,在ECMAScript中,函數是可以封裝在父函數中的,並且可以使用父函數上下文的變數。這個特性會引發funarg問題。
Funarg問題
在面向堆疊的程式語言中,函數的局部變數都是保存在堆疊上的,每當函數啟動的時候,這些變數和函數參數都會壓入到該堆疊上。
當函數回傳的時候,這些參數又會從堆疊中移除。這種模型對將函數作為函數式值使用的時候有很大的限制(比方說,作為返回值從父函數中返回)。絕大部分情況下,問題會出現在函數有自由變數的時候。
自由變數是指在函數中使用的,但既不是函數參數也不是函數的局部變數的變數
例子:
function testFn() { var localVar = 10; function innerFn(innerParam) { alert(innerParam + localVar); } return innerFn; } var someFn = testFn(); someFn(20); // 30
上述例子中,對於innerFn函數來說,localVar就屬於自由變數。
對於採用面向堆疊模型來儲存局部變數的系統而言,就表示當testFn函數呼叫結束後,其局部變數都會從堆疊中移除。這樣一來,當從外部對innerFn進行函數呼叫的時候,就會發生錯誤(因為localVar變數已經不存在了)。
而且,上述例子在面向棧實作模型中,要想將innerFn以傳回值傳回根本是不可能的。因為它也是testFn函數的局部變量,也會隨著testFn的回傳而移除。
還有一個問題是當系統採用動態作用域,函數作為函數參數使用的時候有關。
看如下例子(偽代碼):
var z = 10; function foo() { alert(z); } foo(); // 10 – 使用静态和动态作用域的时候 (function () { var z = 20; foo(); // 10 – 使用静态作用域, 20 – 使用动态作用域 })(); // 将foo作为参数的时候是一样的 (function (funArg) { var z = 30; funArg(); // 10 – 静态作用域, 30 – 动态作用域 })(foo);
我們看到,採用動態作用域,變數(識別碼)的系統是透過變數動態堆疊來管理的。因此,自由變數是在當前活躍的動態鏈中查詢的,而不是在函數創建的時候保存起來的靜態作用域鏈中查詢的。
这样就会产生冲突。比方说,即使Z仍然存在(与之前从栈中移除变量的例子相反),还是会有这样一个问题: 在不同的函数调用中,Z的值到底取哪个呢(从哪个上下文,哪个作用域中查询)?
上述描述的就是两类funarg问题 —— 取决于是否将函数以返回值返回(第一类问题)以及是否将函数当函数参数使用(第二类问题)。
为了解决上述问题,就引入了 闭包的概念。
闭包
闭包是代码块和创建该代码块的上下文中数据的结合。
让我们来看下面这个例子(伪代码):
var x = 20; function foo() { alert(x); // 自由变量"x" == 20 } // 为foo闭包 fooClosure = { call: foo // 引用到function lexicalEnvironment: {x: 20} // 搜索上下文的上下文 };
上述例子中,“fooClosure”部分是伪代码。对应的,在ECMAScript中,“foo”函数已经有了一个内部属性——创建该函数上下文的作用域链。
“lexical”通常是省略的。上述例子中是为了强调在闭包创建的同时,上下文的数据就会保存起来。当下次调用该函数的时候,自由变量就可以在保存的(闭包)上下文中找到了,正如上述代码所示,变量“z”的值总是10。
定义中我们使用的比较广义的词 —— “代码块”,然而,通常(在ECMAScript中)会使用我们经常用到的函数。当然了,并不是所有对闭包的实现都会将闭包和函数绑在一起,比方说,在Ruby语言中,闭包就有可能是: 一个过程对象(procedure object), 一个lambda表达式或者是代码块。
对于要实现将局部变量在上下文销毁后仍然保存下来,基于栈的实现显然是不适用的(因为与基于栈的结构相矛盾)。因此在这种情况下,上层作用域的闭包数据是通过 动态分配内存的方式来实现的(基于“堆”的实现),配合使用垃圾回收器(garbage collector简称GC)和 引用计数(reference counting)。这种实现方式比基于栈的实现性能要低,然而,任何一种实现总是可以优化的: 可以分析函数是否使用了自由变量,函数式参数或者函数式值,然后根据情况来决定 —— 是将数据存放在堆栈中还是堆中。
以上就是JavaScript闭包其一:闭包概论的内容,更多相关内容请关注PHP中文网(www.php.cn)!