成人午夜视频全免费观看高清-秋霞福利视频一区二区三区-国产精品久久久久电影小说-亚洲不卡区三一区三区一区

JavaScript閉包是什么

這篇文章給大家分享的是有關JavaScript閉包是什么的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

在柳江等地區(qū),都構建了全面的區(qū)域性戰(zhàn)略布局,加強發(fā)展的系統(tǒng)性、市場前瞻性、產品創(chuàng)新能力,以專注、極致的服務理念,為客戶提供成都網站設計、網站制作 網站設計制作按需開發(fā),公司網站建設,企業(yè)網站建設,品牌網站設計,營銷型網站,成都外貿網站建設,柳江網站建設費用合理。

概論

在討論ECMAScript閉包之前,先來介紹下函數式編程(與ECMA-262-3 標準無關)中一些基本定義。 然而,為了更好的解釋這些定義,這里還是拿ECMAScript來舉例。

眾所周知,在函數式語言中(ECMAScript也支持這種風格),函數即是數據。就比方說,函數可以保存在變量中,可以當參數傳遞給其他函數,還可以當返回值返回等等。 這類函數有特殊的名字和結構。

定義

函數式參數(“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');
            };
    })()();//這種()直接執(zhí)行的方式要熟悉。

可以以正常數據形式存在的函數(比方說:當參數傳遞,接受函數式參數或者以函數值返回)都稱作 第一類函數(一般說第一類對象)。 在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;
    })(); 

在函數式參數中定義的變量,在“funArg”激活時就能夠訪問了(因為存儲上下文數據的變量對象每次在進入上下文的時候就創(chuàng)建出來了):

function testFn(funArg) {

 // 激活funArg, 本地變量localVar可訪問
 funArg(10); // 20
 funArg(20); // 30

 }

 testFn(function (arg) {

 var localVar = 10;
 alert(arg + localVar);

 });

然而,我們知道,在ECMAScript中,函數是可以封裝在父函數中的,并可以使用父函數上下文的變量。 這個特性會引發(fā) funArg問題。

FunArg問題

面向堆棧的編程語言中,函數的本地變量都是保存在堆棧上的, 每當函數激活的時候,這些變量和函數參數都會壓棧到該堆棧上。

當函數返回的時候,這些參數又會從堆棧中移除。這種模型對將函數作為函數式值使用的時候有很大的限制(比方說,作為返回值從父函數中返回)。 絕大部分情況下,問題會出現在當函數有 自由變量的時候。

自由變量是指在函數中使用的,但既不是函數參數也不是函數的局部變量的變量

如下所示:

function testFn() {

 var localVar = 10;

 function innerFn(innerParam) {
 alert(innerParam + localVar);
 }

 return innerFn;
 }

 var someFn = testFn();
 someFn(20); // 30

上述例子中,對于innerFn函數來說,localVar就屬于自由變量。

對于采用 面向堆棧模型來存儲局部變量的系統(tǒng)而言,就意味著當testFn函數調用結束后,其局部變量都會從堆棧中移除。 這樣一來,當從外部對innerFn進行函數調用的時候,就會發(fā)生錯誤(因為localVar變量已經不存在了)。

而且,上述例子在 面向堆棧實現模型中,要想將innerFn以返回值返回根本是不可能的。 因為它也是testFn函數的局部變量,也會隨著testFn的返回而移除。

還有一個函數對象問題和當系統(tǒng)采用動態(tài)作用域,函數作為函數參數使用的時候有關。

看如下例子:

var z = 10;

 function foo() {
 alert(z);
 }

 foo(); // 10 – 靜態(tài)作用域和動態(tài)作用域情況下都是

 (function () {

 var z = 20;
 foo(); // 10 – 靜態(tài)作用域情況下, 20 – 動態(tài)作用域情況下

 })();

 // 將foo函數以參數傳遞情況也是一樣的

 (function (funArg) {

 var z = 30;
 funArg(); // 10 – 靜態(tài)作用域情況下, 30 – 動態(tài)作用域情況下

 })(foo);

我們看到,采用動態(tài)作用域,變量(標識符)處理是通過動態(tài)堆棧來管理的。 因此,自由變量是在當前活躍的動態(tài)鏈中查詢的,而不是在函數創(chuàng)建的時候保存起來的靜態(tài)作用域鏈中查詢的。

這樣就會產生沖突。比方說,即使Z仍然存在(與之前從堆棧中移除變量的例子相反),還是會有這樣一個問題: 在不同的函數調用中,Z的值到底取哪個呢(從哪個上下文,哪個作用域中查詢)?

上述描述的就是兩類 funArg問題 —— 取決于是否將函數以返回值返回(第一類問題)以及是否將函數當函數參數使用(第二類問題)。

為了解決上述問題,就引入了 閉包的概念。下面重點來了,閉包的面紗就此揭開。

閉包

閉包是代碼塊和創(chuàng)建該代碼塊的上下文中數據的結合。

讓我們來看下面這個例子:

var x = 20;

 function foo() {
 alert(x); // 自由變量 "x" == 20
 }

 // foo的閉包
 fooClosure = {
 call: foo // 對函數的引用
 lexicalEnvironment: {x: 20} // 查詢自由變量的上下文
 };

上述例子中,“fooClosure”部分是偽代碼。對應的,在ECMAScript中,“foo”函數已經有了一個內部屬性——創(chuàng)建該函數上下文的作用域鏈。

這里“l(fā)exical”是不言而喻的,通常是省略的。上述例子中是為了強調在閉包創(chuàng)建的同時,上下文的數據就會保存起來。 當下次調用該函數的時候,自由變量就可以在保存的(閉包)上下文中找到了,正如上述代碼所示,變量“z”的值總是10。

定義中我們使用的比較廣義的詞 —— “代碼塊”,然而,通常(在ECMAScript中)會使用我們經常用到的函數。 當然了,并不是所有對閉包的實現都會將閉包和函數綁在一起,比方說,在Ruby語言中,閉包就有可能是: 一個程序對象(procedure object), 一個lambda表達式或者是代碼塊。

對于要實現將局部變量在上下文銷毀后仍然保存下來,基于堆棧的實現顯然是不適用的(因為與基于堆棧的結構相矛盾)。 因此在這種情況下,上層作用域的閉包數據是通過 動態(tài)分配內存的方式來實現的(基于“堆”的實現),配合使用垃圾回收器(garbage collector簡稱GC)和 引用計數(reference counting)。 這種實現方式比基于堆棧的實現性能要低,然而,任何一種實現總是可以優(yōu)化的: 可以分析函數是否使用了自由變量,函數式參數或者函數式值,然后根據情況來決定 —— 是將數據存放在堆棧中還是堆中。

ECMAScript閉包的實現

討論完理論部分,接下來讓我們來介紹下ECMAScript中閉包究竟是如何實現的。 這里還是有必要再次強調下:ECMAScript只使用靜態(tài)(詞法)作用域(而諸如Perl這樣的語言,既可以使用靜態(tài)作用域也可以使用動態(tài)作用域進行變量聲明)。

var x = 10;

 function foo() {
 alert(x);
 }

 (function (funArg) {

 var x = 20;

 // funArg的變量 "x" 是靜態(tài)保存的,在該函數創(chuàng)建的時候就保存了

 funArg(); // 10, 而不是 20

 })(foo);

從技術角度來說,創(chuàng)建該函數的上層上下文的數據是保存在函數的內部屬性[[Scope]]中的。如果你對[[Scope]]和作用域鏈的知識完全理解了的話,那對閉包也就完全理解了。

根據函數創(chuàng)建的算法,我們看到 在ECMAScript中,所有的函數都是閉包,因為它們都是在創(chuàng)建的時候就保存了上層上下文的作用域鏈(除開異常的情況) (不管這個函數后續(xù)是否會激活 —— [[Scope]]在函數創(chuàng)建的時候就有了):

var x = 10;

 function foo() {
 alert(x);
 }

 // foo is a closure
 foo: FunctionObject = {
 [[Call]]: code block of foo,
 [[Scope]]: [
 global: {
 x: 10
 }
 ],
 ... // other properties
 };

正如此前提到過的,出于優(yōu)化的目的,當函數不使用自由變量的時候,實現層可能就不會保存上層作用域鏈。 然而,ECMAScript-262-3標準中并未對此作任何說明;因此,嚴格來說 —— 所有函數都會在創(chuàng)建的時候將上層作用域鏈保存在[[Scope]]中。

“萬能”的[[Scope]]

這里不得不說JavaScript的作用域鏈

這里還要注意的是:在ECMAScript中,同一個上下文中創(chuàng)建的閉包是共用一個[[Scope]]屬性的。也就是說,某個閉包對其中的變量做修改會影響到其他閉包對其變量的讀?。?/p>

var firstClosure;
 var secondClosure;

 function foo() {

 var x = 1;

 firstClosure = function () { return ++x; };
 secondClosure = function () { return --x; };

 x = 2; // 對AO["x"]產生了影響, 其值在兩個閉包的[[Scope]]中

 alert(firstClosure()); // 3, 通過 firstClosure.[[Scope]]
 }

 foo();

 alert(firstClosure()); // 4
 alert(secondClosure()); // 3

正因為這個特性,很多人都會犯一個非常常見的錯誤: 當在循環(huán)中創(chuàng)建了函數,然后將循環(huán)的索引值和每個函數綁定的時候,通常得到的結果不是預期的(預期是希望每個函數都能夠獲取各自對應的索引值)。這個錯誤也是題目中說提到的那段代碼的最大錯誤的地方,下面我們來揭秘為啥button點擊彈出來的都是5.

var data = [];

 for (var k = 0; k < 3; k++) {
 data[k] = function () {
 alert(k);
 };
 }

 data[0](); // 3, 而不是 0
 data[1](); // 3, 而不是 1
 data[2](); // 3, 而不是 2

 上述例子就證明了 —— 同一個上下文中創(chuàng)建的閉包是共用一個[[Scope]]屬性的。因此上層上下文中的變量“k”是可以很容易就被改變的。

activeContext.Scope = [
 ... // higher variable objects
 {data: [...], k: 3} // activation object
 ];

 data[0].[[Scope]] === Scope;
 data[1].[[Scope]] === Scope;
 data[2].[[Scope]] === Scope;

這樣一來,在函數激活的時候,最終使用到的k就已經變成了3了。

如下所示,創(chuàng)建一個額外的閉包就可以解決這個問題了:

var data = [];

 for (var k = 0; k < 3; k++) {
 data[k] = (function _helper(x) {
 return function () {
 alert(x);
 };
 })(k); // 將 "k" 值傳遞進去
 }

 // 現在就對了
 data[0](); // 0
 data[1](); // 1
 data[2](); // 2

 上述例子中,函數“_helper”創(chuàng)建出來之后,通過參數“k”激活。其返回值也是個函數,該函數保存在對應的數組元素中。 這種技術產生了如下效果: 在函數激活時,每次“_helper”都會創(chuàng)建一個新的變量對象,其中含有參數“x”,“x”的值就是傳遞進來的“k”的值。 這樣一來,返回的函數的[[Scope]]就成了如下所示:

data[0].[[Scope]] === [
 ... // 更上層的變量對象
 上層上下文的AO: {data: [...], k: 3},
 _helper上下文的AO: {x: 0}
 ];

 data[1].[[Scope]] === [
 ... // 更上層的變量對象
 上層上下文的AO: {data: [...], k: 3},
 _helper上下文的AO: {x: 1}
 ];

 data[2].[[Scope]] === [
 ... // 更上層的變量對象
 上層上下文的AO: {data: [...], k: 3},
 _helper上下文的AO: {x: 2}
 ];

我們看到,這個時候函數的[[Scope]]屬性就有了真正想要的值了,為了達到這樣的目的,我們不得不在[[Scope]]中創(chuàng)建額外的變量對象。 要注意的是,在返回的函數中,如果要獲取“k”的值,那么該值還是會是3。

順便提下,大量介紹JavaScript的文章都認為只有額外創(chuàng)建的函數才是閉包,這種說法是錯誤的(我也差點有這個錯誤的認識,再次感謝作者指出。)。 實踐得出,這種方式是最有效的,然而,從理論角度來說,在ECMAScript中所有的函數都是閉包。

然而,上述提到的方法并不是唯一的方法。通過其他方式也可以獲得正確的“k”的值,如下所示:

var data = [];

 for (var k = 0; k < 3; k++) {
 (data[k] = function () {
 alert(arguments.callee.x);
 }).x = k; // 將“k”存儲為函數的一個屬性
 }

 // 同樣也是可行的
 data[0](); // 0
 data[1](); // 1
 data[2](); // 2

然而,arguments.callee從ECMAScript (ES5)中移除了,所以這個方法了解下就行了。

FunArg和return

另外一個特性是從閉包中返回。在ECMAScript中,閉包中的返回語句會將控制流返回給調用上下文(調用者)。 而在其他語言中,比如,Ruby,有很多中形式的閉包,相應的處理閉包返回也都不同,下面幾種方式都是可能的:可能直接返回給調用者,或者在某些情況下——直接從上下文退出。

ECMAScript標準的退出行為如下:

function getElement() {

 [1, 2, 3].forEach(function (element) {

 if (element % 2 == 0) {
 // 返回給函數"forEach",
 // 而不會從getElement函數返回
 alert('found: ' + element); // found: 2
 return element;
 }

 });

 return null;
 }

 alert(getElement()); // null, 而不是 2

 然而,在ECMAScript中通過try catch可以實現如下效果(這個方法簡直太棒了!):

var $break = {};

 function getElement() {

 try {

 [1, 2, 3].forEach(function (element) {

 if (element % 2 == 0) {
 // 直接從getElement"返回"
 alert('found: ' + element); // found: 2
 $break.data = element;
 throw $break;
 }

 });

 } catch (e) {
 if (e == $break) {
 return $break.data;
 }
 }

 return null;
 }

 alert(getElement()); // 2
 理論版本

通常,程序員會錯誤的認為,只有匿名函數才是閉包。其實并非如此,正如我們所看到的 —— 正是因為作用域鏈,使得所有的函數都是閉包(與函數類型無關: 匿名函數,FE,NFE,FD都是閉包), 這里只有一類函數除外,那就是通過Function構造器創(chuàng)建的函數,因為其[[Scope]]只包含全局對象。 為了更好的澄清該問題,我們對ECMAScript中的閉包作兩個定義(即兩種閉包):

ECMAScript中,閉包指的是:

  • 從理論角度:所有的函數。因為它們都在創(chuàng)建的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,因為函數中訪問全局變量就相當于是在訪問自由變量,這個時候使用最外層的作用域。

  • 從實踐角度:以下函數才算是閉包:

                ?即使創(chuàng)建它的上下文已經銷毀,它仍然存在(比如,內部函數從父函數中返回)

                ?在代碼中引用了自由變量

閉包實踐

實際使用的時候,閉包可以創(chuàng)建出非常優(yōu)雅的設計,允許對funArg上定義的多種計算方式進行定制。 如下就是數組排序的例子,它接受一個排序條件函數作為參數:

[1, 2, 3].sort(function (a, b) {
        ... // 排序條件
    });

同樣的例子還有,數組的map方法(并非所有的實現都支持數組map方法,SpiderMonkey從1.6版本開始有支持),該方法根據函數中定義的條件將原數組映射到一個新的數組中:  

[1, 2, 3].map(function (element) {
        return element * 2;
    }); // [2, 4, 6]

使用函數式參數,可以很方便的實現一個搜索方法,并且可以支持無窮多的搜索條件:  

 someCollection.find(function (element) {
        return element.someProperty == 'searchCondition';
    });

還有應用函數,比如常見的forEach方法,將funArg應用到每個數組元素:   

[1, 2, 3].forEach(function (element) {
        if (element % 2 != 0) {
            alert(element);
        }
    }); // 1, 3

 順便提下,函數對象的 apply 和 call方法,在函數式編程中也可以用作應用函數。這里,我們將它們看作是應用函數 —— 應用到參數中的函數(在apply中是參數列表,在call中是獨立的參數): 

(function () {
        alert([].join.call(arguments, ';')); // 1;2;3
    }).apply(this, [1, 2, 3]);

閉包還有另外一個非常重要的應用 —— 延遲調用:   

 var a = 10;
    setTimeout(function () {
        alert(a); // 10, 一秒鐘后
    }, 1000);

也可以用于回調函數:

...
 var x = 10;
 // only for example
 xmlHttpRequestObject.onreadystatechange = function () {
 // 當數據就緒的時候,才會調用;
 // 這里,不論是在哪個上下文中創(chuàng)建,變量“x”的值已經存在了
 alert(x); // 10
 };
 ..

還可以用于封裝作用域來隱藏輔助對象:

var foo = {};

 // initialization
 (function (object) {

 var x = 10;

 object.getX = function _getX() {
 return x;
 };

 })(foo);

 alert(foo.getX()); // get closured "x" – 10
幾個閉包樣例

例子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()

因此執(zhí)行結果應該彈出的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; }
 }
 setupSomeGlobals(); // 為三個全局變量賦值
 gAlertNumber(); //666
 gIncreaseNumber();
 gAlertNumber(); // 667
 gSetNumber(12); //
 gAlertNumber(); //12

 例子3:當在一個循環(huán)中賦值函數時,這些函數將綁定同樣的閉包

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的執(zhí)行結果是彈出item3 undefined窗口三次,因為這三個函數綁定了同一個閉包,而且item的值為最后計算的結果,但是當i跳出循環(huán)時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();

執(zhí)行結果是彈出”Hello Alice”的窗口。即使局部變量聲明在函數sayAlert之后,局部變量仍然可以被訪問到。

例子5:每次函數調用的時候創(chuàng)建一個新的閉包

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訪 問內部私有函數。需要注意的地方是匿名主函數結束的地方的'()',如果沒有這個'()'就不能產生單件。因為匿名函數只能返回了唯一的對象,而且不能被 其他地方調用。這個就是利用閉包產生單件的方法。

感謝各位的閱讀!關于“JavaScript閉包是什么”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

網站標題:JavaScript閉包是什么
當前網址:http://jinyejixie.com/article12/ggipdc.html

成都網站建設公司_創(chuàng)新互聯,為您提供品牌網站設計品牌網站制作、品牌網站建設、全網營銷推廣、外貿網站建設、企業(yè)建站

廣告

聲明:本網站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯

綿陽服務器托管
久治县| 云浮市| 沾益县| 密云县| 洛宁县| 榕江县| 长阳| 乐东| 车致| 彰化县| 沛县| 清徐县| 清水河县| 和顺县| 淮安市| 汽车| 平利县| 赤水市| 岳池县| 新干县| 家居| 云和县| 东乡族自治县| 德兴市| 前郭尔| 阳高县| 英德市| 汉源县| 眉山市| 甘肃省| 陵水| 凉山| 疏附县| 安图县| 五常市| 台安县| 涞水县| 涟水县| 乌什县| 红河县| 永寿县|