js中關於閉包的詳細介紹
1. 什麼是閉包?
來看一些關於閉包的定義:
#閉包是指有權存取另一個函數作用域中變數的函數- -《JS高階程式設計第三版》 p178
函數物件可以透過作用域鏈相關聯起來,函數體內部的變數都可以保存在函數作用域內,這種特性稱為'閉包' 。 --《JS權威指南》 p183
內部函數可以存取定義它們的外部函數的參數和變數(除了
this
和arguments
)。 --《JS語言精粹》 p36
來個定義總結
可以存取外部函數作用域中變量的
函數
被內部函數存取的外部函數的變數可以保存在外部函數作用域內而不被回收---這是核心,後面我們遇到閉包都要想到,我們要專注在被閉包引用的這個變數。
來建立一個簡單的閉包
var sayName = function(){var name = 'jozo';return function(){ alert(name); } };var say = sayName(); say();
來解讀後面兩個語句:
var say = sayName()
:返回了一個匿名的內部函數保存在變數say中,並且引用了外部函數的變數name,由於垃圾回收機制,sayName函數執行完畢後,變數name並沒有被銷毀。say()
:執行傳回的內部函數,仍能存取變數name,輸出'jozo' .
2. 閉包中的作用域鏈
理解作用域鏈對理解閉包也很有幫助。
變數在作用域中的找出方式應該都很熟悉了,其實這就是順著作用域鏈往上找的。
當函數被呼叫時:
先建立一個執行環境(execution context),及對應的作用域鏈;
將arguments和其他命名參數的值加入函數的活動物件(activation object)
作用域鏈:目前函數的活動物件優先權最高,外部函數的活動物件次之,外部函數的外部函數的活動物件依次遞減,直至作用域鏈的末端--全域作用域。優先順序就是變數尋找的順序;
先來看個普通的作用域鏈:
function sayName(name){return name; }var say = sayName('jozo');
這段程式碼包含兩個作用域: a.
全域作用域;b.
sayName函數的作用域,也就是只有兩個變數對象,當執行到對應的執行環境時,變數物件會變成活動對象,並被推入到執行環境作用域鏈的前端,也就是成為優先順序最高的那個。 看圖片說話:
這圖在JS高階程式設計書上也有,我重新繪了遍。
在創建sayName()函數時,會建立一個預先包含變數物件的作用域鏈,也就是圖中索引為1的作用域鏈,並且被儲存到內部的[[Scope]]屬性中,當呼叫sayName()函數的時候,會建立一個執行環境,然後透過複製函數的[[Scope]]屬性中的物件建立起作用域鏈,此後,又有一個活動物件(圖中索引為0 )被創建,並被推入執行環境作用域鏈的前端。
一般來說,當函數執行完畢後,局部活動物件就會被銷毀,記憶體中只保存全域作用域。但是,閉包的情況又有所不同:
再來看看看閉包的作用域鏈:
function sayName(name){return function(){return name; } }var say = sayName('jozo');
這個閉包實例比上一個例子多了一個匿名函數的作用域:
在匿名函式從sayName()函式中被傳回後,它的作用域鏈被初始化為包含sayName()函數的活動物件和全域變數物件。這樣,匿名函數就可以存取在sayName()中定義的所有變數和參數,更為重要的是,sayName()函數在執行完畢後,其活動物件也不會被銷毀,因為匿名函數的作用域鏈依然在引用這個活動對象,換句話說,sayName()函數執行完後,其執行環境的作用域鏈會被銷毀,但他的活動對象會留在記憶體中,知道匿名函數會銷毀。這個也是後面要講到的記憶體外洩的問題。
作用域鏈問題不寫那麼多了,寫書上的東西也很累o(╯□╰)o
3. 閉包的實例
實例1:實現累加
// 方式1var a = 0;var add = function(){ a++; console.log(a) }add();add();//方式2 :闭包var add = (function(){ var a = 0; return function(){ a++; console.log(a); } })(); console.log(a); //undefinedadd();add(); 相比之下方式2更加优雅,也减少全局变量,将变量私有化
實例2 :給每個li添加點擊事件
var oli = document.getElementsByTagName('li'); var i; for(i = 0;i < 5;i++){ oli[i].onclick = function(){ alert(i); } } console.log(i); // 5 //执行匿名函数 (function(){ alert(i); //5 }());
上面是一個經典的例子,我們都知道執行結果是都彈出5,也知道可以用閉包解決這個問題,但是我剛開始始終不能明白為什麼每次彈出都是5,為什麼閉包可以解決這個問題。後來捋一捋還是把它弄清晰了:
a. 先来分析没用闭包前的情况:for循环中,我们给每个li点击事件绑定了一个匿名函数,匿名函数中返回了变量i的值,当循环结束后,变量i的值变为5,此时我们再去点击每个li,也就是执行相应的匿名函数(看上面的代码),这是变量i已经是5了,所以每个点击弹出5. 因为这里返回的每个匿名函数都是引用了同一个变量i,如果我们新建一个变量保存循环执行时当前的i的值,然后再让匿名函数应用这个变量,最后再返回这个匿名函数,这样就可以达到我们的目的了,这就是运用闭包来实现的!
b. 再来分析下运用闭包时的情况:
var oli = document.getElementsByTagName('li'); var i; for(i = 0;i < 5;i++){ oli[i].onclick = (function(num){ var a = num; // 为了说明问题 return function(){ alert(a); } })(i) } console.log(i); // 5
这里for循环执行时,给点击事件绑定的匿名函数传递i后立即执行返回一个内部的匿名函数,因为参数是按值传递的,所以此时形参num保存的就是当前i的值,然后赋值给局部变量 a,然后这个内部的匿名函数一直保存着a的引用,也就是一直保存着当前i的值。 所以循环执行完毕后点击每个li,返回的匿名函数执行弹出各自保存的 a 的引用的值。
4. 闭包的运用
我们来看看闭包的用途。事实上,通过使用闭包,我们可以做很多事情。比如模拟面向对象的代码风格;更优雅,更简洁的表达出代码;在某些方面提升代码的执行效率。
1. 匿名自执行函数
我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,比如UI的初始化,那么我们可以使用闭包:
//将全部li字体变为红色 (function(){ var els = document.getElementsByTagName('li');for(var i = 0,lng = els.length;i < lng;i++){ els[i].style.color = 'red'; } })();
我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,
因此els,i,lng这些局部变量在执行完后很快就会被释放,节省内存!
关键是这种机制不会污染全局对象。
2. 实现封装/模块化代码
var person= function(){ //变量作用域为函数内部,外部无法访问 var name = "default"; return { getName : function(){ return name; }, setName : function(newName){ name = newName; } } }();console.log(person.name);//直接访问,结果为undefined console.log(person.getName()); //default person.setName("jozo"); console.log(person.getName()); //jozo
3. 实现面向对象中的对象
这样不同的对象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的机制,但是通过使用闭包,
我们可以模拟出这样的机制。还是以上边的例子来讲:
function Person(){ var name = "default"; return { getName : function(){ return name; }, setName : function(newName){ name = newName; } } }; var person1= Person(); print(person1.getName()); john.setName("person1"); print(person1.getName()); // person1 var person2= Person(); print(person2.getName()); jack.setName("erson2"); print(erson2.getName()); //person2
Person的两个实例person1 和 person2 互不干扰!因为这两个实例对name这个成员的访问是独立的 。
5. 内存泄露及解决方案
垃圾回收机制
说到内存管理,自然离不开JS中的垃圾回收机制,有两种策略来实现垃圾回收:标记清除 和 引用计数;
标记清除:
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量的标记和被环境中的变量引用的变量的标记,此后,如果变量再被标记则表示此变量准备被删除。 2008年为止,IE,Firefox,opera,chrome,Safari的javascript都用使用了该方式;
引用计数:
跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型的值赋给该变量时,这个值的引用次数就是1,如果这个值再被赋值给另一个变量,则引用次数加1。相反,如果一个变量脱离了该值的引用,则该值引用次数减1,当次数为0时,就会等待垃圾收集器的回收。
这个方式存在一个比较大的问题就是循环引用,就是说A对象包含一个指向B的指针,对象B也包含一个指向A的引用。 这就可能造成大量内存得不到回收(内存泄露),因为它们的引用次数永远不可能是 0 。早期的IE版本里(ie4-ie6)采用是计数的垃圾回收机制,闭包导致内存泄露的一个原因就是这个算法的一个缺陷。
我们知道,IE中有一部分对象并不是原生额javascript对象,例如,BOM和DOM中的对象就是以COM对象的形式实现的,而COM对象的垃圾回收机制采用的就是引用计数。因此,虽然IE的javascript引擎采用的是标记清除策略,但是访问COM对象依然是基于引用计数的,因此只要在IE中设计COM对象就会存在循环引用的问题!
举个栗子:
window.onload = function(){var el = document.getElementById("id"); el.onclick = function(){ alert(el.id); } }
这段代码为什么会造成内存泄露?
el.onclick= function () { alert(el.id); };
执行这段代码的时候,将匿名函数对象赋值给el的onclick属性;然后匿名函数内部又引用了el对象,存在循环引用,所以不能被回收;
解决方法:
window.onload = function(){var el = document.getElementById("id");var id = el.id; //解除循环引用 el.onclick = function(){ alert(id); } el = null; // 将闭包引用的外部函数中活动对象清除 }
6. 总结闭包的优缺点
优点:
可以让一个变量常驻内存 (如果用的多了就成了缺点
避免全局变量的污染
私有化变量
缺点
因為閉包會攜帶包含它的函數的作用域,因此會比其他函數佔用更多的記憶體
- ##會造成記憶體外洩 #
以上是js中關於閉包的詳細介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

熱門話題

對於機械硬碟、或SATA固態硬碟,軟體運轉速度的提升會有感覺,如果是NVME硬碟,可能感覺不到。一,註冊表導入桌面新建一個文字文檔,複製貼上如下內容,另存為1.reg,然後右鍵合併,並重新啟動電腦。 WindowsRegistryEditorVersion5.00[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SessionManager\MemoryManagement]"DisablePagingExecutive"=d

typedef struct 在 C 語言中用於建立結構體類型別名,簡化結構體使用。它透過指定結構體別名將一個新的資料類型作為現有結構體的別名。優點包括增強可讀性、程式碼重複使用和類型檢查。注意:在使用別名前必須定義結構體,別名在程式中必須唯一且僅在其宣告的作用域內有效。

本站9月3日消息,韓媒etnews當地時間昨報道稱,三星電子和SK海力士的「類HBM式」堆疊結構行動記憶體產品將在2026年後實現商業化。消息人士表示這兩大韓國記憶體巨頭將堆疊式行動記憶體視為未來重要收入來源,並計劃將「類HBM記憶體」擴展到智慧型手機、平板電腦和筆記型電腦中,為端側AI提供動力。綜合本站先前報導,三星電子的此類產品叫做LPWideI/O內存,SK海力士則將這方面技術稱為VFO。兩家企業使用了大致相同的技術路線,即將扇出封裝和垂直通道結合在一起。三星電子的LPWideI/O內存位寬達512

JavaScript 閉包的優點包括維持變數作用域、實作模組化程式碼、延遲執行和事件處理;缺點包括記憶體洩漏、增加了複雜性、效能開銷和作用域鏈影響。

C++ 中的 #include 預處理器指令將外部來源檔案的內容插入到目前原始檔案中,以複製其內容到目前原始檔案的相應位置。主要用於包含頭文件,這些頭文件包含程式碼中所需的聲明,例如 #include <iostream> 是包含標準輸入/輸出函數。

本站6月7日消息,金邦(GEIL)在2024台北國際電腦展上推出了其最新DDR5解決方案,而且給出了SO-DIMM、CUDIMM、CSODIMM、CAMM2和LPCAMM2等版本可選。 ▲圖來源:Wccftech如圖所示,金邦展出的CAMM2/LPCAMM2記憶體採用非常緊湊的設計,最高可提供128GB的容量,速度最高可達8533MT/s,其中部分產品甚至可以在AMDAM5平台上穩定超頻至9000MT/s,且無需任何輔助散熱。據介紹,金邦2024款PolarisRGBDDR5系列記憶體最高可提供8400

C++智慧指標的生命週期:建立:分配記憶體時建立智慧指標。所有權轉移:透過移動操作轉移所有權。釋放:智慧指標離開作用域或被明確釋放時釋放記憶體。物件銷毀:所指向物件被銷毀時,智慧型指標成為無效指標。

本站7月23日消息,微電子標準制定方JEDEC固態技術協會當地時間22日宣布,DDR5MRDIMM和LPDDR6CAMM內存技術規範即將正式推出,並介紹了這兩項內存的關鍵細節。 DDR5MRDIMM中的「MR」即MultiplexedRank(多路復用列),這意味著此記憶體支援兩個或以上的Rank(列),並可在單一通道上組合和傳輸多個資料訊號,無需額外的物理連線就能有效提升頻寬。 JEDEC規劃了多代DDR5MRDIMM內存,目標最終將其頻寬提升至12.8Gbps,較DDR5RDIMM內存目前的6.4
