什麼是閉包
閉包是什麼?閉包是Closure,這是靜態語言所不具有的一個新特性。但閉包也不是什麼複雜到不可理解的東西,簡而言之,閉包就是:
閉包就是函數的局部變數集合,只是這些局部變數在函數傳回後會繼續存在。
閉包就是就是函數的「堆疊」在函數返回後並不釋放,我們也可以理解為這些函數堆疊並不在棧上分配而是在堆上分配當在一個函數內定義另外一個函數就會產生閉包上面的第二定義是第一個補充說明,抽取第一個定義的主謂賓-閉包是函數的'局部變數'集合。只是這個局部變數是可以在函數返回後被存取。 (這個不是官方定義,但這個定義應該更有利於你理解閉包)
理解Javascript的閉包非常關鍵,本篇試圖用最簡單的例子來理解此概念。
function greet(sth){ return function(name){ console.log(sth + ' ' + name); } } //hi darren greet('hi')('darren');
或可以寫成這樣:
var sayHi = greet('hi'); sayHi('darren');
我們要提的問題是:為什麼greet的內部函數可以使用sth這個變數?
其內部大致運作如下:
→ 建立全域上下文
→ 執行var sayHi = greet('hi');語句,建立greet上下文,變數sth儲存在greet上下文。
→ 繼續執行greet函數內的語句,傳回一個匿名函數,雖然greet上下文從堆疊上消失,但sth變數依舊存在於記憶體的某個空間。
→ 繼續執行sayHi('darren');創建了sayHi上下文,並試圖搜尋sth變量,但在sayHi這個上下文中沒有sth變量。 sayHi上下文會沿著一個作用域鏈找到sth變數對應的那個記憶體。 外層函數就像一個閉包,其內部函數可以使用外部函數的變數。
一個閉包的簡單例子
function buildFunctions(){ var funcArr = []; for(var i = 0; i < 3; i++){ funcArr.push(function(){console.log(i)}); } return funcArr; } var fs = buildFunctions(); fs[0](); //3 fs[1](); //3 fs[2](); //3
以上,為什麼結果不是0, 1, 2呢?
--因為i作為一個閉包變量,當前值為3,被內部函數使用。要實現想要的效果,可以在遍歷的時候每一次遍歷創建一個獨立的上下文使其不受閉包影響。而自觸發函數可以實現獨立上下文。
function buildFunctions(){ var funcArr = []; for(var i = 0; i < 3; i++){ funcArr.push((function(j){ return function(){ console.log(j); }; }(i))); } return funcArr; } var fs = buildFunctions(); fs[0](); //0 fs[1](); //1 fs[2](); //2
本篇的兩個例子正好體現了閉包的2個面向:一個是內部函數使用閉包變量,另一個是把內部函數寫在自觸發函數中從而避免受閉包影響。
做為局部變數都可以被函數內的程式碼訪問,這個和靜態語言是沒有差別。閉包的差異在於局部變變數可以在函數執行結束後仍然被函數外的程式碼存取。這意味著 著函數必須傳回一個指向閉包的“引用”,或將這個”引用”賦值給某個外部變量,才能保證閉包中局部變數被外部程式碼存取。當然包含這個引用的實體應該是一個 對象,因為在Javascript中除了基本型別剩下的就都是對象了。可惜的是,ECMAScript並沒有提供相關的成員和方法來存取閉包中的局部變 量。但是在ECMAScript中,函數物件中定義的內部函數() inner function是可以直接存取外部函數的局部變量,透過這個機制,我們就可以以如下的方式完成對閉包的存取了。
function greeting(name) { var text = 'Hello ' + name; // local variable // 每次调用时,产生闭包,并返回内部函数对象给调用者 return function () { alert(text); } } var sayHello=greeting( "Closure" ); sayHello() // 通过闭包访问到了局部变量text
上述代码的执行结果是:Hello Closure,因为sayHello()函数在greeting函数执行完毕后,仍然可以访问到了定义在其之内的局部变量text。
好了,这个就是传说中闭包的效果,闭包在Javascript中有多种应用场景和模式,比如Singleton,Power Constructor等这些Javascript模式都离不开对闭包的使用。
ECMAScript闭包模型
ECMAScript到底是如何实现闭包的呢?想深入了解的亲们可以获取ECMAScript 规范进行研究,我这里也只做一个简单的讲解,内容也是来自于网络。
在ECMAscript的脚本的函数运行时,每个函数关联都有一个执行上下文场景(Execution Context) ,这个执行上下文场景中包含三个部分
文法环境(The LexicalEnvironment)
变量环境(The VariableEnvironment)
this绑定
其中第三点this绑定与闭包无关,不在本文中讨论。文法环境中用于解析函数执行过程使用到的变量标识符。我们可以将文法环境想象成一个对象,该对 象包含了两个重要组件,环境记录(Enviroment Recode),和外部引用(指针)。环境记录包含包含了函数内部声明的局部变量和参数变量,外部引用指向了外部函数对象的上下文执行场景。全局的上下文 场景中此引用值为NULL。这样的数据结构就构成了一个单向的链表,每个引用都指向外层的上下文场景。
例如上面我们例子的闭包模型应该是这样,sayHello函数在最下层,上层是函数greeting,最外层是全局场景。如下图:
因此当sayHello被调用的时候,sayHello会通过上下文场景找到局部变量text的值,因此在屏幕的对话框中显示出”Hello Closure”
变量环境(The VariableEnvironment)和文法环境的作用基本相似,具体的区别请参看ECMAScript的规范文档。
闭包的样列
前面的我大致了解了Javascript闭包是什么,闭包在Javascript是怎么实现的。下面我们通过针对一些例子来帮助大家更加深入的理解闭包,下面共有5个样例,例子来自于JavaScript Closures For Dummies(镜像)。
例子1:闭包中局部变量是引用而非拷贝
function say667() { // Local variable that ends up within closure var num = 666; var sayAlert = function() { alert(num); } num++; return sayAlert; } var sayAlert = say667(); sayAlert()
因此执行结果应该弹出的667而非666。
例子2:多个函数绑定同一个闭包,因为他们定义在同一个函数内。
function setupSomeGlobals() { // Local variable that ends up within closure var num = 666; // Store some references to functions as global variables gAlertNumber = function() { alert(num); } gIncreaseNumber = function() { num++; } gSetNumber = function(x) { num = x; } } setupSomeGolbals(); // 为三个全局变量赋值 gAlertNumber(); //666 gIncreaseNumber(); gAlertNumber(); // 667 gSetNumber(12);// gAlertNumber();//12
例子3:当在一个循环中赋值函数时,这些函数将绑定同样的闭包
function buildList(list) { var result = []; for (var i = 0; i < list.length; i++) { var item = 'item' + list[i]; result.push( function() {alert(item + ' ' + list[i])} ); } return result; } function testList() { var fnlist = buildList([1,2,3]); // using j only to help prevent confusion - could use i for (var j = 0; j < fnlist.length; j++) { fnlist[j](); } }
testList的执行结果是弹出item3 undefined窗口三次,因为这三个函数绑定了同一个闭包,而且item的值为最后计算的结果,但是当i跳出循环时i值为4,所以list[4]的结果为undefined.
例子4:外部函数所有局部变量都在闭包内,即使这个变量声明在内部函数定义之后。
function sayAlice() { var sayAlert = function() { alert(alice); } // Local variable that ends up within closure var alice = 'Hello Alice'; return sayAlert; } var helloAlice=sayAlice(); helloAlice();
执行结果是弹出”Hello Alice”的窗口。即使局部变量声明在函数sayAlert之后,局部变量仍然可以被访问到。
例子5:每次函数调用的时候创建一个新的闭包
function newClosure(someNum, someRef) { // Local variables that end up within closure var num = someNum; var anArray = [1,2,3]; var ref = someRef; return function(x) { num += x; anArray.push(num); alert('num: ' + num + '\nanArray ' + anArray.toString() + '\nref.someVar ' + ref.someVar); } } closure1=newClosure(40,{someVar:'closure 1'}); closure2=newClosure(1000,{someVar:'closure 2'}); closure1(5); // num:45 anArray[1,2,3,45] ref:'someVar closure1' closure2(-10);// num:990 anArray[1,2,3,990] ref:'someVar closure2'
闭包的应用
Singleton 单件:
var singleton = function () { var privateVariable; function privateFunction(x) { ...privateVariable... } return { firstMethod: function (a, b) { ...privateVariable... }, secondMethod: function (c) { ...privateFunction()... } }; }();
这个单件通过闭包来实现。通过闭包完成了私有的成员和方法的封装。匿名主函数返回一个对象。对象包含了两个方法,方法1可以方法私有变量,方法2访 问内部私有函数。需要注意的地方是匿名主函数结束的地方的'()',如果没有这个'()'就不能产生单件。因为匿名函数只能返回了唯一的对象,而且不能被 其他地方调用。这个就是利用闭包产生单件的方法。