摘要:在前端開發(fā)中閉包是一個很重要的知識點,是面試中一定會被問到的內(nèi)容。閉包的用途閉包可以用在許多地方。這里僅僅是我對閉包的一些見解,若有錯誤的地方,還望大家提出,一起交流共同進步參考文獻你不知道的上卷深入理解系列
在前端開發(fā)中閉包是一個很重要的知識點,是面試中一定會被問到的內(nèi)容。之前我對閉包的理解主要是"通過閉包可以在函數(shù)外部能訪問到函數(shù)內(nèi)部的變量",對閉包運用的也很少,甚至自己寫過閉包自己都不太清楚,只知道這樣寫可以解決問題。最近在梳理自己的js知識點,發(fā)現(xiàn)自己對js閉包理解的很不透徹,于是想全面的分析一下閉包,特別是閉包的形成原因和閉包的使用場景。
閉包的定義閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù) --《JavaScript高級程序設(shè)計》
函數(shù)對象可以通過作用域關(guān)聯(lián)起來,函數(shù)體內(nèi)的變量都可以保存在函數(shù)作用域內(nèi),這在計算機科學(xué)文獻中稱為“閉包”,所有的javascirpt函數(shù)都是閉包 --《Javascript權(quán)威指南》
看完這些專業(yè)的定義是不是感覺一頭霧水,沒關(guān)系,我和一樣也沒明白這些定義說的是啥,咱接著往下分析。
在認識閉包原理之前我們先必須對作用域、執(zhí)行上下文、執(zhí)行上下文堆棧、變量對象、活動對象、作用域鏈有著全面的認識
作用域 Scope作用域是一套規(guī)則,用于確定在何處以及如何查找變量(標識符)
作用域共有兩種主要的工作模型:
詞法作用域:作用域是在編寫代碼的時候確定的
動態(tài)作用域:作用域是在代碼運行的時候確定的
我們知道javascript使用的是詞法作用域
執(zhí)行上下文 Execution ContextsJavascript中代碼的執(zhí)行上下文分為以下三種:
全局級別的代碼 – 這個是默認的代碼運行環(huán)境,一旦代碼被載入,引擎最先進入的就是這個環(huán)境。
函數(shù)級別的代碼 – 當(dāng)執(zhí)行一個函數(shù)時,運行函數(shù)體中的代碼。
Eval的代碼 – 在Eval函數(shù)內(nèi)運行的代碼。
一個執(zhí)行的上下文可以抽象的理解為一個對象。每一個執(zhí)行的上下文都有一系列的屬性(變量對象(variable object),this指針(this value),作用域鏈(scope chain) )
Execution Contexts = { variable object:變量對象; this value: this指針; scope chain:作用域鏈; }執(zhí)行上下文堆棧 Execution Contexts Stack
活動的執(zhí)行上下文組在邏輯上組成一個堆棧。堆棧底部永遠都是全局上下文(globalContext),而頂部就是當(dāng)前(活動的)執(zhí)行上下文。
當(dāng)add函數(shù)被調(diào)用時,add函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端,此時執(zhí)行上下文堆棧可表示為:
EC Stack = [functionContext globalContext ];
add函數(shù)執(zhí)行完畢后,其執(zhí)行上下文將會從執(zhí)行上下文堆棧頂端彈出并被銷毀。全局執(zhí)行上下文只有在瀏覽器關(guān)閉時才會從執(zhí)行上下文堆棧中銷毀
變量對象 Variable Object如果變量與執(zhí)行上下文相關(guān),那變量自己應(yīng)該知道它的數(shù)據(jù)存儲在哪里,并且知道如何訪問。這種機制稱為變量對象(variable object)。
可以說變量對象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域(scope of data) 。它是與執(zhí)行上下文關(guān)聯(lián)的特殊對象,用于存儲被定義在執(zhí)行上下文中的變量(variables)、函數(shù)聲明(function declarations) 。
當(dāng)進入全局上下文時,全局上下文的變量對象可表示為:
VO = { add:活動對象 Activation Object, sum: undefined, Math: <...>, String: <...> ... window: global //引用自身 }
當(dāng)函數(shù)被調(diào)用者激活時,這個特殊的活動對象(activation object) 就被創(chuàng)建了。它包含普通參數(shù)(formal parameters) 與特殊參數(shù)(arguments)對象(具有索引屬性的參數(shù)映射表)。活動對象在函數(shù)上下文中作為變量對象使用。
當(dāng)add函數(shù)被調(diào)用時,add函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端,add函數(shù)執(zhí)行上下文中活動對象可表示為
AO = { num: 4, sum :5, arguments:{0:4} }作用域鏈 Scope Chain
函數(shù)上下文的作用域鏈在函數(shù)調(diào)用時創(chuàng)建的,包含活動對象AO和這個函數(shù)內(nèi)部的[[scope]]屬性。
var x = 10; function foo() { var y = 20; function bar() { var z = 30; alert(x + y + z); } bar(); } foo();
在這段代碼中我們看到變量"y"在函數(shù)"foo"中定義(意味著它在foo上下文的AO中)"z"在函數(shù)"bar"中定義,但是變量"x"并未在"bar"上下文中定義,相應(yīng)地,它也不會添加到"bar"的AO中。乍一看,變量"x"相對于函數(shù)"bar"根本就不存在;
函數(shù)"bar"如何訪問到變量"x"?理論上函數(shù)應(yīng)該能訪問一個更高一層上下文的變量對象。實際上它正是這樣,這種機制是通過函數(shù)內(nèi)部的[[scope]]屬性來實現(xiàn)的。
[[scope]]是所有父級變量對象的層級鏈,處于當(dāng)前函數(shù)上下文之上,在函數(shù)創(chuàng)建時存于其中。
注意: [[scope]]在函數(shù)創(chuàng)建時被存儲是靜態(tài)的(不變的),直至函數(shù)銷毀。即:函數(shù)可以永不調(diào)用,但[[scope]]屬性已經(jīng)寫入,并存儲在函數(shù)對象中。
在這里我們逐步分析下
全局上下文的變量對象是:
globalContext.VO === Global = { x: 10 foo:};
在"foo"創(chuàng)建時,"foo"的[[scope]]屬性是:
foo.[[Scope]] = [ globalContext.VO ];
在"foo"激活時(進入上下文),"foo"上下文的活動對象是:
fooContext.AO = { y: 20, bar:};
"foo"上下文的作用域鏈為:
fooContext.Scope = [ fooContext.AO, globalContext.VO ];
內(nèi)部函數(shù)"bar"創(chuàng)建時,其[[scope]]為:
bar.[[Scope]] = [ fooContext.AO, globalContext.VO ];
在"bar"激活時,"bar"上下文的活動對象為:
barContext.AO = { z: 30 };
"bar"上下文的作用域鏈為:
bar.Scope= [ barContext.AO, fooContext.AO, globalContext.VO ];
介紹了一大堆內(nèi)容,是不是有點暈忽忽的?堅持一下,下面是重點
閉包的原理我們通過一個閉包的例子來分析一下閉包的形成原理
function add(){ var sum =5; var func = function () { console.log(sum); } return func; } var addFunc = add(); addFunc(); //5
js執(zhí)行流進入全局執(zhí)行上下文環(huán)境時,全局執(zhí)行上下文可表示為:
globalContext = { VO: { add:, addFunc: undefined }, this: window, scope chain: window }
當(dāng)add函數(shù)被調(diào)用時,add函數(shù)執(zhí)行上下文可表示為:
addContext = { AO: { sum: undefined //代碼進入執(zhí)行階段時此處被賦值為5 func: undefined //代碼進入執(zhí)行階段時此處被賦值為function (){console.log(sum);} }, this: window, scope chain: addContext.AO + globalContext.VO }
add函數(shù)執(zhí)行完畢后,js執(zhí)行流回到全局上下文環(huán)境中,將add函數(shù)的返回值賦值給addFunc。
由于addFunc仍保存著func函數(shù)的引用,所以add函數(shù)執(zhí)行上下文從執(zhí)行上下文堆棧頂端彈出后并未被銷毀而是保存在內(nèi)存中。
當(dāng)addFunc()執(zhí)行時,func函數(shù)被調(diào)用,此時func函數(shù)執(zhí)行上下文可表示為:
funcContext = { this: window, scope chain: addContext.AO + globalContext.VO }
當(dāng)要訪問變量sum時,func的活動對象中未能找到,則會沿著作用域鏈查找,由于js遵循詞法作用域,作用域在函數(shù)創(chuàng)建階段就被確定,在add函數(shù)的活動對象中找到sum = 5;
介紹到這里你明白形成閉包的原因了嗎?
Javascript允許使用內(nèi)部函數(shù)---即函數(shù)定義和函數(shù)表達式位于另一個函數(shù)的函數(shù)體內(nèi)。而且,這些內(nèi)部函數(shù)可以訪問它們所在的外部函數(shù)中聲明的所有局部變量、參數(shù)和聲明的其他內(nèi)部函數(shù)。當(dāng)其中一個這樣的內(nèi)部函數(shù)在包含它們的外部函數(shù)之外被調(diào)用時,就會形成閉包。
閉包可以用在許多地方。它的最大用處有兩個,一個是前面提到的可以讀取函數(shù)內(nèi)部的變量,另一個就是讓這些變量的值始終保持在內(nèi)存中。
1. 保護變量的安全實現(xiàn)JS私有屬性和私有方法利用閉包可以讀取函數(shù)內(nèi)部的變量,變量在函數(shù)外部不能直接讀取到,從而達到保護變量安全的作用。因為私有方法在函數(shù)內(nèi)部都能被訪問到,從而實現(xiàn)了私有屬性和方法的共享。
常見的模塊模式就是利用閉包的這種特性建立的
var Counter = (function() { //私有屬性 var privateCounter = 0; //私有方法 function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } })(); console.log(privateCounter); //privateCounter is not defined console.log(Counter.value()); // 0 Counter.increment(); Counter.increment(); console.log(Counter.value()); // 2 Counter.decrement(); console.log(Counter.value()); // 1
在jQuery框架的私有方法和變量也是這么設(shè)計的
var $ = jQuery = function(){ return jQuery.fn.init(); } jQuery.fn = jQuery.prototype = { init:function(){ return this; //this指向jQuery.prototype }, length: 1, size: function(){ return this.length; } } console.log($().size()); // 12. 將處理結(jié)果緩存
var CachedSearchBox = (function(){ var cache = {},count = []; return { attachSearchBox : function(dsid){ if(dsid in cache){//如果結(jié)果在緩存中 return cache[dsid];//直接返回緩存中的對象 } var fsb = new uikit.webctrl.SearchBox(dsid);//新建 cache[dsid] = fsb;//更新緩存 if(count.length > 100){//保正緩存的大小<=100 delete cache[count.shift()]; } return fsb; } }; })(); CachedSearchBox.attachSearchBox("input");
這樣我們在第二次調(diào)用的時候,就會從緩存中讀取到該對象。
理解了閉包的原理我們發(fā)現(xiàn)閉包的這些用途都是利用了閉包保存了當(dāng)前函數(shù)的活動對象的特點,這樣閉包函數(shù)在作用域之外被調(diào)用時依然能夠訪問其創(chuàng)建時的作用域
閉包的缺點閉包將函數(shù)的活動對象維持在內(nèi)存中,過度使用閉包會導(dǎo)致內(nèi)存占用過多,所以在使用完后需要將保存在內(nèi)存中的活動對象解除引用;
閉包只能取得外部函數(shù)中任何變量的最后一個值,在使用循環(huán)且返回的函數(shù)中帶有循環(huán)變量時會得到錯誤結(jié)果;
當(dāng)返回的函數(shù)為匿名函數(shù)時,注意匿名函數(shù)中的this指的是window對象。
這里僅僅是我對閉包的一些見解,若有錯誤的地方,還望大家提出,一起交流共同進步!
參考文獻
《你不知道的JavaScript》上卷
深入理解JavaScript系列
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/83917.html
摘要:當(dāng)在中調(diào)用匿名函數(shù)時,它們用的都是同一個閉包,而且在這個閉包中使用了和的當(dāng)前值的值為因為循環(huán)已經(jīng)結(jié)束,的值為。最好將閉包當(dāng)作是一個函數(shù)的入口創(chuàng)建的,而局部變量是被添加進這個閉包的。 閉包不是魔法 這篇文章使用一些簡單的代碼例子來解釋JavaScript閉包的概念,即使新手也可以輕松參透閉包的含義。 其實只要理解了核心概念,閉包并不是那么的難于理解。但是,網(wǎng)上充斥了太多學(xué)術(shù)性的文章,對于...
摘要:閉包引起的內(nèi)存泄漏總結(jié)從理論的角度將由于作用域鏈的特性中所有函數(shù)都是閉包但是從應(yīng)用的角度來說只有當(dāng)函數(shù)以返回值返回或者當(dāng)函數(shù)以參數(shù)形式使用或者當(dāng)函數(shù)中自由變量在函數(shù)外被引用時才能成為明確意義上的閉包。 文章同步到github js的閉包概念幾乎是任何面試官都會問的問題,最近把閉包這塊的概念梳理了一下,記錄成以下文章。 什么是閉包 我先列出一些官方及經(jīng)典書籍等書中給出的概念,這些概念雖然...
摘要:也許最好的理解是閉包總是在進入某個函數(shù)的時候被創(chuàng)建,而局部變量是被加入到這個閉包中。在函數(shù)內(nèi)部的函數(shù)的內(nèi)部聲明函數(shù)是可以的可以獲得不止一個層級的閉包。 前言 總括 :這篇文章使用有效的javascript代碼向程序員們解釋了閉包,大牛和功能型程序員請自行忽略。 譯者 :文章寫在2006年,可直到翻譯的21小時之前作者還在完善這篇文章,在Stackoverflow的How do Java...
摘要:閉包在我理解是一種比較抽象的東西。所以我寫了一篇博文來方便自己理解閉包。那么現(xiàn)在我們可以解釋一下閉包的第一個定義在計算機科學(xué)中,閉包是引用了自由變量的函數(shù)。循環(huán)中創(chuàng)建閉包在我們使用的關(guān)鍵字之前,閉包的一個常見問題就出現(xiàn)在循環(huán)中創(chuàng)建閉包。 零. 前言 從我開始接觸前端時就聽說過閉包,但是一直不理解閉包究竟是什么。上網(wǎng)看了各種博客,大家對閉包的說法不一。閉包在我理解是一種比較抽象的東西。所...
摘要:但是閉包也不是什么復(fù)雜到不可理解的東西,簡而言之,閉包就是閉包就是函數(shù)的局部變量集合,只是這些局部變量在函數(shù)返回后會繼續(xù)存在。可惜的是,并沒有提供相關(guān)的成員和方法來訪問閉包中的局部變量。 (收藏自 技術(shù)狂) 前言:還是一篇入門文章。Javascript中有幾個非常重要的語言特性——對象、原型繼承、閉包。其中閉包 對于那些使用傳統(tǒng)靜態(tài)語言C/C++的程序員來說是一個新的語言特性。本文將...
摘要:到底什么是閉包這個問題在面試是時候經(jīng)常都會被問,很多小白一聽就懵逼了,不知道如何回答好。上面這么說閉包是一種特殊的對象。閉包的注意事項通常,函數(shù)的作用域及其所有變量都會在函數(shù)執(zhí)行結(jié)束后被銷毀。從而使用閉包模塊化代碼,減少全局變量的污染。 閉包,有人說它是一種設(shè)計理念,有人說所有的函數(shù)都是閉包。到底什么是閉包?這個問題在面試是時候經(jīng)常都會被問,很多小白一聽就懵逼了,不知道如何回答好。這個...
閱讀 2321·2021-09-22 15:27
閱讀 3174·2021-09-03 10:32
閱讀 3504·2021-09-01 11:38
閱讀 2501·2019-08-30 15:56
閱讀 2217·2019-08-30 13:01
閱讀 1540·2019-08-29 12:13
閱讀 1423·2019-08-26 13:33
閱讀 896·2019-08-26 13:30