摘要:請用一句話描述什么是閉包,并寫出代碼進行說明。閉包的使用場景使用閉包可以在中模擬塊級作用域閉包可以用于在對象中創(chuàng)建私有變量。
引言
剛學(xué)習(xí)前端的時候,看到閉包這個詞,總是一臉懵逼,面試的時候,問到這個問題,也是回答的含含糊糊,總感覺有層隔膜,覺得這個概念很神奇,如果能掌握,必將功力大漲。其實,閉包沒有這么神秘,它無處不在。
一個簡短的的問題首先,來看一個問題。
請用一句話描述什么是閉包,并寫出代碼進行說明。
如果能毫不猶豫的說出來,并能給出解釋,那下面文字對你來說就沒有往下看的必要了。
就這個問題,結(jié)合我查閱的資料和經(jīng)驗,在這里簡單的說一下,如果哪里有不對的,歡迎指正。
先回答上面的問題,什么是閉包。
閉包是一個概念,它描述了函數(shù)執(zhí)行完畢后,依然駐留內(nèi)存的現(xiàn)象。
代碼描述:
function foo() { var a = 2; function bar(){ console.log(a); } return bar; } var test = foo(); test(); //2
上面這段代碼,清晰的展示了閉包。
函數(shù) bar() 的詞法作用域能夠訪問 foo()的內(nèi)部作用域。然后我們將bar()函數(shù)本身當作一個值類型進行傳遞。上面這個例子,我們將bar() 所引用的函數(shù)對象本身作為返回值。
foo() 執(zhí)行完畢之后, 其內(nèi)部作用域并沒有被銷毀,因為bar()依然保持著對內(nèi)部作用域的引用,拜bar()的位置所賜,它擁有涵蓋foo()內(nèi)部作用域的閉包,使得該作用域能夠一直存活,以供bar()在之后的任何時間進行引用。這個引用,其實就是閉包。
也正是這個原因,test被實際調(diào)用的時候,它可以訪問定義時的詞法作用域,所以,才能訪問到a.
函數(shù)傳遞也可以是間接的:
var fn; function foo(){ var a = 2; function baz() { console.log( a ); } fn = baz; //將baz 分配給全局變量 } function bar(){ fn(); } foo(); bar(); //2
所以,無論通過何種手段將內(nèi)部函數(shù)傳遞到其所在的詞法作用域外,它都會持有對原始定義作用域的引用。也就是說,無論在什么地方執(zhí)行這個函數(shù),都會使用閉包。也是這個原因,我們才可以很方便的使用回調(diào)函數(shù)而不用關(guān)心其具體細節(jié)。
其實,在定時器,事件監(jiān)聽器,ajax請求, 跨窗口通信,Web Workers 或者任何其他的同步 或 異步任務(wù)中,只要使用了回調(diào)函數(shù),實際上就是在使用閉包。
到這里,或許你已經(jīng)對閉包有個大概的了解,下面我再舉幾個例子來幫你加深對閉包的認識。
幾個更具體的例子首先,就先看一下所謂的立即執(zhí)行函數(shù).
var a = 2; (function IIFE() { console.log(a); })(); //2
這個立即執(zhí)行函數(shù)通常被認為是經(jīng)典的閉包例子,它可以正常工作,但嚴格意義上講,它并不是閉包。
為什么呢?
因為這個IIFE函數(shù)并不是在它本身的詞法作用域之外執(zhí)行的。它在定義時所在的作用域中執(zhí)行了。而且,變量a 是通過普通的詞法作用域查找的,而不是通過閉包。
另一個用來說明閉包的例子是循環(huán)。
some text1 some text2 some text3
var handler = function(nodes) { for(var i = 0, l = nodes.length; i < l ; i++) { nodes[i].onclick = function(){ console.log(i); } } } var tabs = document.querySelectorAll(".tabs .tab"); handler(tabs);
我們預(yù)期的結(jié)果是log 0 ,1,2;
執(zhí)行之后的結(jié)果卻是是三個3;
這是為什么呢?
首先解釋下這個3是怎么來的,
看一下循環(huán)體,循環(huán)的終止條件是 i < l , 首次條件成立時 i 的值是3 。
因此 ,輸出顯示的是循環(huán)結(jié)束時 i 的最終值。 根據(jù)作用域的工作原理,盡管循環(huán)中的函數(shù)是在各個迭代中分別定義的,但是它們都被封閉在一個共享的全局作用域中,因此實際上是只有一個i.
handler 函數(shù)的本意是想把唯一的i傳遞給事件處理器,但是失敗了。
因為事件處理器函數(shù)綁定了 i本身 ,而不是函數(shù)在構(gòu)造時的i的值.
知道了這個之后,我們可以做出相應(yīng)的調(diào)整:
var handler = function(nodes) { var helper = function(i){ return function(e){ console.log(i); // 0 1 2 } } for(var i = 0, l = nodes.length; i < l ; i++) { nodes[i].onclick = helper(i); } }
在循環(huán)外創(chuàng)建一個輔助函數(shù),讓這個輔助函數(shù)在返回一個綁定了當前i的值的函數(shù),這樣就不會混淆了。
明白了這點,就會發(fā)現(xiàn),上面的處理就是為了創(chuàng)建一個新的作用域,換句話說,每次迭代我們都需要一個塊作用域.
說到塊作用域,就不得不提一個詞,那就是let.
所以,如果你不想過多的使用閉包,就可以使用let:
var handler = function(nodes) { for(let i = 0, l = nodes.length; i < l ; i++) { //nodes[i].index = i; nodes[i].onclick = function(){ console.log(i); // 0 1 2 } } }jQuery中的閉包
先來看個例子
var sel = $("#con"); setTimeout( function (){ sel.css({background:"gray"}); }, 2000);
上邊的代碼使用了 jQuery 的選擇器,找到 id 為 con 的元素,注冊計時器,兩秒之后,將背景色設(shè)置為灰色。
這個代碼片段的神奇之處在于,在調(diào)用了 setTimeout 函數(shù)之后,con 依舊被保持在函數(shù)內(nèi)部,當兩秒鐘之后,id 為 con 的 div 元素的背景色確實得到了改變。應(yīng)該注意的是,setTimeout 在調(diào)用之后已經(jīng)返回了,但是 con 沒有被釋放,這是因為 con 引用了全局作用域里的變量 con。
以上的例子幫助我們了解了更多關(guān)于閉包的細節(jié),下面我們就深入閉包世界探尋一番。
深入理解閉包首先看一個概念-執(zhí)行上下文(Execution Context)。
執(zhí)行上下文是一個抽象的概念,ECMAScript 規(guī)范使用它來追蹤代碼的執(zhí)行。它可能是你的代碼第一次執(zhí)行或執(zhí)行的流程進入函數(shù)主體時所在的全局上下文。
在任意一個時間點,只能有唯一一個執(zhí)行上下文在運行之中。
這就是為什么 JavaScript 是“單線程”的原因,意思就是一次只能處理一個請求。
一般來說,瀏覽器會用棧來保存這個執(zhí)行上下文。
棧是一種“后進先出” (Last In First Out) 的數(shù)據(jù)結(jié)構(gòu),即最后插入該棧的元素會最先從棧中被彈出(這是因為我們只能從棧的頂部插入或刪除元素)。
當前的執(zhí)行上下文,或者說正在運行中的執(zhí)行上下文永遠在棧頂。
當運行中的上下文被完全執(zhí)行以后,它會由棧頂彈出,使得下一個棧頂?shù)捻椊犹嫠蔀檎谶\行的執(zhí)行上下文。
除此之外,一個執(zhí)行上下文正在運行并不代表另一個執(zhí)行上下文需要等待它完成運行之后才可以開始運行。
有時會出現(xiàn)這樣的情況,一個正在運行中的上下文暫停或中止,另外一個上下文開始執(zhí)行。暫停的上下文可能在稍后某一時間點從它中止的位置繼續(xù)執(zhí)行。
一個新的執(zhí)行上下文被創(chuàng)建并推入棧頂,成為當前的執(zhí)行上下文,這就是執(zhí)行上下文替代的機制。
當我們有很多執(zhí)行上下文一個接一個地運行時——通常情況下會在中間暫停然后再恢復(fù)運行——為了能很好地管理這些上下文的順序和執(zhí)行情況,我們需要用一些方法來對其狀態(tài)進行追蹤。而實際上也是如此,根據(jù)ECMAScript的規(guī)范,每個執(zhí)行上下文都有用于跟蹤代碼執(zhí)行進程的各種狀態(tài)的組件。包括:
代碼執(zhí)行狀態(tài):任何需要開始運行,暫停和恢復(fù)執(zhí)行上下文相關(guān)代碼執(zhí)行的狀態(tài)
函數(shù):上下文中正在執(zhí)行的函數(shù)對象(正在執(zhí)行的上下文是腳本或模塊的情況下可能是null)
Realm:一系列內(nèi)部對象,一個ECMAScript全局環(huán)境,所有在全局環(huán)境的作用域內(nèi)加載的ECMAScript代碼,和其他相關(guān)的狀態(tài)及資源。
詞法環(huán)境:用于解決此執(zhí)行上下文內(nèi)代碼所做的標識符引用。
變量環(huán)境:一種詞法環(huán)境,該詞法環(huán)境的環(huán)境記錄保留了變量聲明時在執(zhí)行上下文中創(chuàng)建的綁定關(guān)系。
模塊與閉包現(xiàn)在的開發(fā)都離不開模塊化,下面說說模塊是如何利用閉包的。
先看一個實際中的例子。
這是一個統(tǒng)計模塊,看一下代碼:
define("components/webTrends", ["webTrendCore"], function(require,exports, module) { var webTrendCore = require("webTrendCore"); var webTrends = { init:function (obj) { var self = this; self.dcsGetId(); self.dcsCollect(); }, dcsGetId:function(){ if (typeof(_tag) != "undefined") { _tag.dcsid="dcs5w0txb10000wocrvqy1nqm_6n1p"; _tag.dcsGetId(); } }, dcsCollect:function(){ if (typeof(_tag) != "undefined") { _tag.DCSext.platform="weimendian"; if(document.readyState!="complete"){ document.onreadystatechange = function(){ if(document.readyState=="complete") _tag.dcsCollect() } } else _tag.dcsCollect() } } }; module.exports = webTrends; })
在主頁面使用的時候,調(diào)用一下就可以了:
var webTrends = require("webTrends"); webTrends.init();
在定義的模塊中,我們暴露了webTrends對象,在外面調(diào)用返回對象中的方法就形成了閉包。
模塊的兩個必要條件:
必須有外部的封閉函數(shù),該函數(shù)必須至少被調(diào)用一次
封閉函數(shù)必須返回至少一個內(nèi)部函數(shù),這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態(tài)。
性能考量如果一個任務(wù)不需要使用閉包,那最好不要在函數(shù)內(nèi)創(chuàng)建函數(shù)。
原因很明顯,這會 拖慢腳本的處理速度,加大內(nèi)存消耗 。
舉個例子,當需要創(chuàng)建一個對象時,方法通常應(yīng)該和對象的原型關(guān)聯(lián),而不是定義到對象的構(gòu)造函數(shù)中。 原因是 每次構(gòu)造函數(shù)被調(diào)用, 方法都會被重新賦值 (即 對于每個對象創(chuàng)建),這顯然是一種不好的做法。
看一個能說明問題,但是不推薦的做法:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
上面的代碼并沒有很好的利用閉包,我們來改進一下:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype = { getName: function() { return this.name; }, getMessage: function() { return this.message; } };
好一些了,但是不推薦重新定義原型,再來改進下:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype.getName = function() { return this.name; }; MyObject.prototype.getMessage = function() { return this.message; };
很顯然,在現(xiàn)有的原型上添加方法是一種更好的做法。
上面的代碼還可以寫的更簡練:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } (function() { this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }).call(MyObject.prototype);
在前面的三個示例中,繼承的原型可以由所有對象共享,并且在每個對象創(chuàng)建時不需要定義方法定義。如果想看更多細節(jié),可以參考對象模型。
閉包的使用場景:使用閉包可以在JavaScript中模擬塊級作用域;
閉包可以用于在對象中創(chuàng)建私有變量。
閉包的優(yōu)缺點優(yōu)點:
邏輯連續(xù),當閉包作為另一個函數(shù)調(diào)用的參數(shù)時,避免你脫離當前邏輯而多帶帶編寫額外邏輯。
方便調(diào)用上下文的局部變量。
加強封裝性,第2點的延伸,可以達到對變量的保護作用。
缺點:
內(nèi)存浪費。這個內(nèi)存浪費不僅僅因為它常駐內(nèi)存,對閉包的使用不當會造成無效內(nèi)存的產(chǎn)生。
結(jié)語前面對閉包做了一些簡單的解釋,最后再總結(jié)下,其實閉包沒什么特別的,其特點是:
函數(shù)嵌套函數(shù)
函數(shù)內(nèi)部可以訪問到外部的變量或者對象
避免了垃圾回收
歡迎交流,以上 ;-)
參考資料讓我們一起學(xué)習(xí)JavaScript閉包吧
弄懂JavaScript的作用域和閉包
Closures
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/81435.html
摘要:大名鼎鼎的閉包面試必問。閉包的作用是什么。看到閉包在哪了嗎閉包到底是什么五年前,我也被這個問題困擾,于是去搜了并總結(jié)下來。關(guān)于閉包的謠言閉包會造成內(nèi)存泄露錯。閉包里面的變量明明就是我們需要的變量,憑什么說是內(nèi)存泄露這個謠言是如何來的因為。 本文為饑人谷講師方方原創(chuàng)文章,首發(fā)于 前端學(xué)習(xí)指南。 大名鼎鼎的閉包!面試必問。請用自己的話簡述 什么是「閉包」。 「閉包」的作用是什么。 首先...
摘要:本文作為第三篇,將會討論另一個開發(fā)者容易忽視的重要主題內(nèi)存管理。我們也會提供一些關(guān)于如何處理內(nèi)存泄露的技巧。這是當前整型和雙精度的大小。然而,這是一組可以收集的內(nèi)存空間的近似值。 本文轉(zhuǎn)載自:眾成翻譯譯者:Leslie Wang審校: 為之漫筆鏈接:http://www.zcfy.cc/article/4211原文:https://blog.sessionstack.com/how-j...
摘要:一什么是閉包閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。就是創(chuàng)建了一個匿名函數(shù)調(diào)用函數(shù)解除對匿名函數(shù)的引用,以便釋放內(nèi)存 一、什么是閉包? 閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。創(chuàng)建閉包的常見方式就是在一個函數(shù)內(nèi)部創(chuàng)建另一個函數(shù)。 二、為什么要閉包 說明:變量分為全局變量的局部變量,全局變量的作用域為全局作用域,局部變量作用域為局部作用域。之前一篇文章關(guān)于作用域鏈給了介紹...
摘要:吐槽一下,閉包這個詞的翻譯真是有很大的誤解性啊要說閉包,要先說下詞法作用域。閉包兩個作用通過閉包,在外部環(huán)境訪問內(nèi)部環(huán)境的變量。閉包使得函數(shù)可以繼續(xù)訪問定義時的詞法作用域。 閉包是真的讓人頭暈啊,看了很久還是覺得很模糊。只能把目前自己的一些理解先寫下來,這其中必定包含著一些錯誤,待日后有更深刻的理解時再作更改。 吐槽一下,閉包這個詞的翻譯真是有很大的誤解性啊…… 要說閉包,要先說下詞法...
閱讀 2973·2021-10-27 14:16
閱讀 695·2021-10-13 09:39
閱讀 3669·2021-09-29 09:46
閱讀 2090·2019-08-30 15:54
閱讀 2597·2019-08-30 15:52
閱讀 2994·2019-08-30 15:44
閱讀 1103·2019-08-30 15:44
閱讀 497·2019-08-30 10:51