成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

淺談對JavaScript閉包的理解

開發 前端
事實上當函數當做值類型并到處傳遞時, 基本都會使用閉包,如定時器,跨窗口通信,事件監聽,ajax等等 基本只要使用了回調函數, 實際上就是在使用閉包。閉包是一把雙刃劍 是JavaScript比較難以理解和掌握的部分, 它十分強大,卻也有很大的缺陷,如何使用它完全取決于你自己。

[[171667]]

在談閉包之前,我們首先要了解幾個概念:

  • 什么是函數表達式? 與函數聲明有何不同?
  • JavaScript查找標識符的機制
  • JavaScript的作用域是詞法作用域
  • JavaScript的垃圾回收機制

先來說說函數表達式

什么是函數表達式? 如果function是聲明中的***個詞,那么就是函數聲明,否則就是函數表達式。

舉個例子:

  1. var foo = function(){}; //匿名函數表達式 
  2.  
  3. function foo(){})() //函數表達式,因為function不是聲明中的***個詞,前面還有一個“(” 
  4.  
  5. function foo(){} //函數聲明  

函數表達式也分匿名函數表達式和具名函數表達式:

  1. var foo = function(){} //匿名函數表達式 
  2.  
  3. var foo = function bar(){} //具名函數表達式  

具名函數表達式要注意一點:上例中的bar標識符 只在當前的函數作用域中存在,在全局作用域中是不存在的。

函數聲明與函數表達式的重要區別有:

  • 函數聲明具有函數聲明提升,函數表達式不會被提升
  • 函數表達式可以在表達式后跟個括號來立即執行,函數聲明不行
  1. function (){})() //匿名函數表達式,且立即執行 

這種模式的函數,通常稱為IIFE(Immediately Invoked Function Expresstion)代表立即執行函數表達式。

關于函數、變量聲明的提升這里就不再多說了, 想了解的同學可以查閱一下相關資料

關于JavaScript執行函數時查找標識符的機制

不了解作用域鏈及變量對象的同學可以先查閱相關資料后再來看。

作用域鏈本質上是一個由指向變量對象的指針列表,它只引用但不實際包含變量對象,變量,函數等等都存在各自作用域的變量對象中,通過訪問變量對象來訪問它們。

只有在函數調用的時候,才會創建執行環境和作用域鏈,同時每個環境都只能逐級向上搜索作用域鏈,來查詢變量和函數名等標識符。

JavaScript的作用域

JavaScript的作用域就是詞法作用域而不是動態作用域

詞法作用域最重要的特征是它的定義過程發生在代碼的書寫階段

動態作用域的作用域鏈是基于調用棧的 詞法作用域的作用域鏈是基于代碼中的作用域嵌套

  1. function foo(){ 
  2.     console.log(num) 
  3.     
  4. function bar(){ 
  5.     var num = 2; 
  6.     foo(); // 1 
  7.      
  8. var num = 1; 
  9. bar();      

bar函數執行時,會執行foo函數,因為JavaScript是詞法作用域,所以函數執行時,會沿著定義時的作用域鏈查找變量,而不是執行時,foo函數定義在全局中,所以查找到了全局的num,輸出了1而不是2。

下面來說閉包

關于什么是閉包,其實有很多種說法,這取決于各自的理解,最主要的有兩種:

  • Nicolas C.Zakas:閉包是指有權訪問另一個函數作用域中的變量的函數
  • KYLE SIMPSON:當函數可以記住并訪問所在的詞法作用域時,就產生了閉包,這個函數持有對該詞法作用域的引用,這個引用就叫做閉包

我個人更傾向于后者對于閉包的定義,即閉包是一個引用。下面來看一些代碼:

  1. function foo() { 
  2.     var a = 5; 
  3.     return function() { 
  4.     console.log(a); 
  5.     } 
  6.  } 
  7.  
  8. var bar = foo(); 
  9. bar();       // 5  

這段代碼里 foo執行時會返回一個匿名函數表達式,這個函數能夠訪問foo()的作用域,并且引用能引用它,然后將這個匿名函數賦值給了變量bar,讓bar能引用這個匿名函數并且可以調用它。

這個例子,匿名函數在自己定義的詞法作用域以外的地方成功執行。

這正是閉包強大的地方,比如通過閉包實現模塊模式: 

  1. function aModule() { 
  2.  
  3.     var sometext = "module"
  4.      
  5.     function doSomething() { 
  6.         console.log(sometext); 
  7.     } 
  8.      
  9.     return { 
  10.         doSomething: doSomething 
  11.         }; 
  12.  
  13. var obj = aModule(); 
  14. obj.doSomething()   //module  

我們通過調用aModule函數創建了一個模塊實例,函數返回的這個對象,實質上可以看做是這個模塊的公告API,是不是有些像其它面向對象語言中的class?

再來通過閉包實現一個單例模式: 

  1. var application = function() { 
  2.      
  3.     var components = []; 
  4.     /* 
  5.     一些初始化操作 
  6.     */ 
  7.     return {              //公共API 
  8.         getComponentCount: function() { 
  9.         return components.length; 
  10.         }, 
  11.         registerComponent: function(component) { 
  12.         components.push(component); 
  13.         } 
  14.     }; 
  15. }();  

這個例子通過IIFE創建了一個單例對象,函數里返回的對象字面量是這個單例模式的公共接口。

通過閉包實現模塊模式,可以做到很多強大的事情,模塊模式能成功實現,最關鍵的是返回的API還能繼續引用定義時所在的作用域,從而進行一些操作,也就是說,作用域并沒有因為函數執行后被銷毀,也就是沒有被內存回收,之所以沒有被回收是因為閉包的存在和JavaScript的垃圾回收機制。

JavaScript的垃圾回收機制

JavaScript最常用的垃圾收集方式是標記清除,垃圾收集器會給存儲在內存中的所有變量都加上標記,然后去除環境中的變量,以及被環境中的變量引用的變量的標記,說明這些變量還有作用,暫時不能被刪除,然后在此之后被加上標記的變量就是要刪除的變量了,等待垃圾收集器對他們完成清除工作。

對函數來說,函數執行完畢后,會自動釋放掉里面的變量,可是如果函數內部存在閉包,它們就不會被刪除,因為這個函數還在被內部的函數所引用,所以他不會被加上標記,不會被清除,而是會一直存在內存中得不到釋放!除非使用閉包的那個內部函數被銷毀,外部函數才能得到釋放

所以,雖然閉包強大,但是我們不能濫用它,且在沒有必要的情況下盡量不要創建閉包,不然將會有大量的變量對象得不到釋放,過度占用內存。

關于循環和閉包

當循環和閉包結合在一起時,經常會產生讓初學者覺得匪夷所思的問題。來看一段Nicolas C.Zakas 在《JavaScript高級程序設計》中的代碼: 

  1. function createFunction() { 
  2.     var result = []; 
  3.     for (var i = 0; i < 10; i++) { 
  4.         result[i] = function() { 
  5.             return i; 
  6.         }; 
  7.     } 
  8.     return result; 
  9.  

這個函數執行后,會創建一個由十個函數組成的數組,并且產生十個互不相干的函數作用域,表面上看調用第幾個函數就會輸出幾,但是結果并不是這樣 

  1. var result = createFunction(); 
  2. result[0]();  // 10 
  3. result[9]();  // 10  

產生這種奇怪的現象的原因就是之前說的,createFunction的變量對象因為閉包的存在沒有被釋放,注意閉包保存的是整個變量對象,而不是只保存只被引用的變量,在createFunction執行后,創建了十個函數,同時變量 i 沒有被釋放,依然保存在內存中,所以此時它的值保留為停止循環后的10。

當我們在外部調用函數時,函數沿著它的作用域鏈開始搜索所需要的變量,前面說過,JavaScript的作用域鏈是基于定義時的作用域嵌套,所以當我們調用某個函數比如 result[0] 它就會首先在自己的作用域里通過RSH搜索 i ,顯然 i 不存在這個作用域中,于是它又沿著作用域鏈向上一級作用域中搜索 i ,然后找到了 i ,但是此時createFunction函數已經執行,循環也已經執行完畢了, i 的值為10,所以獲取到的i,值就為10,同理,其他的函數執行時,查找的i 也會是10, 所以每個函數執行結果都是輸出10。

關鍵所在就是盡管循環中的十個函數是在各自的迭代中分別定義的,但是它們都處于一個共享的上一級作用域中,所以它們獲取到的都是一個 i

所以解決此類問題的關鍵就是讓函數查找i時,不找到createFunction的變量對象那一級 ,因為一旦向上搜索到createFunction那里,得到的就是10。所以我們可以通過一些方法在中間來截斷本該搜索到createFunction變量對象的一次查找。

首先我們可以這樣: 

  1. function createFunction() { 
  2.     var result = []; 
  3.     for (var i = 0; i < 10; i++) { 
  4.     (function (){ 
  5.         result[i] = function() { 
  6.             return i; 
  7.         };})(); 
  8.     } 
  9.     return result; 
  10.  

我們通過定義一個立即執行函數表達式,在result[i]函數上一級創建了一個塊級作用域,如果我們把這個塊級作用域叫做a,那么它查找i時是這樣一條鏈 result[i]->a->createFunction,之所以還會查找到createFunction中,是因為a中沒有i這個變量,所以我們需要做些什么,讓它搜索到a時就停下 

  1. function createFunctions() { 
  2.     var result = new Array(); 
  3.     for (var i = 0; i < 10; i++) { 
  4.         (function(i){ 
  5.         result[i] = function() { 
  6.             return i; 
  7.         };})(i); 
  8.         } 
  9.      
  10.     return result; 
  11.  

現在a這個塊級作用域里定義了一個變量 i ,這個 i 與上級的 i 不會互相影響,因為它們存在各自的作用域里, 同時我們將該次迭代時的 i 值賦給了 a這個塊級作用域里的 i ,即a中的 i 保存了當次迭代的 i ,result[i]在外部執行時,是這樣的調用鏈result i -> a在a中就能找到需要的變量,不需要再向上搜索,也不會查找到值為10的 i ,所以調用哪個result[i]函數,就會輸出哪個 i 。

在 ES6 中我們還可以使用 let 來解決此類問題 

  1. function createFunction() { 
  2.     var result = []; 
  3.     for (var i = 0; i < 10; i++) { 
  4.         let j = i; 
  5.         result[i] = function() { 
  6.             return j; 
  7.         }; 
  8.     } 
  9.     return result; 
  10. //輸出一下 
  11. console.log(createFunction()[2]());  //2  

let會創建一個塊級作用域,并在這個作用域中聲明一個變量。所以我們相當于在result[i]上套了一層塊級作用域 

  1. function createFunction() { 
  2.     var result = []; 
  3.     for (var i = 0; i < 10; i++) { 
  4.         //塊的開始 
  5.         let j = i; 
  6.         result[i] = function() { 
  7.             return j; 
  8.         }; 
  9.         //塊的結束 
  10.     } 
  11.     return result; 
  12.  

這種方式解決此類問題,與前面沒有多大分別,總之就是為了不讓函數調用時去查找到最上級的那個 i 。

其實,如果在for循環頭部來進行let聲明還會有一個有趣的行為: 

  1. function createFunction() { 
  2.     var result = []; 
  3.     for (let i = 0; i < 10; i++) {    //每次迭代,都會聲明一次i,總共聲明10次 
  4.         result[i] = function() { 
  5.             return i; 
  6.         }; 
  7.     } 
  8.     return result; 
  9. console.log(createFunction()[2]());  //2  

這樣在for頭部使用let聲明, 每次迭代都會進行聲明,隨后每次迭代都會使用上一個迭代結束時的值來初始化這個變量。

事實上當函數當做值類型并到處傳遞時, 基本都會使用閉包,如定時器,跨窗口通信,事件監聽,ajax等等 基本只要使用了回調函數, 實際上就是在使用閉包。

閉包是一把雙刃劍 是JavaScript比較難以理解和掌握的部分, 它十分強大,卻也有很大的缺陷,如何使用它完全取決于你自己。

以上皆為個人觀點 如若有誤 還望指正。

責任編輯:龐桂玉 來源: segmentfault
相關推薦

2011-03-02 12:33:00

JavaScript

2017-05-22 16:08:30

前端開發javascript閉包

2021-02-21 16:21:19

JavaScript閉包前端

2016-10-27 19:26:47

Javascript閉包

2016-09-14 09:20:05

JavaScript閉包Web

2009-07-24 17:30:37

Javascript閉

2011-08-05 09:33:30

Func局部變量作用域

2009-04-24 09:43:09

.NETASP.NET框架

2022-10-24 08:08:27

閉包編譯器

2020-10-14 15:15:28

JavaScript(

2011-05-25 14:48:33

Javascript閉包

2011-03-22 10:16:48

程序員

2010-06-23 10:24:42

Javascript閉

2012-11-29 10:09:23

Javascript閉包

2017-09-14 13:55:57

JavaScript

2011-05-12 18:26:08

Javascript作用域

2009-06-04 09:41:50

struts2上傳文件

2017-07-14 10:55:05

2021-01-13 11:25:12

JavaScript閉包函數

2009-03-17 15:36:29

JavaScript循環事件
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 日本一区二区三区精品视频 | 亚洲一区二区高清 | 黄色免费在线观看网址 | 老司机成人在线 | 国产精品高潮呻吟久久aⅴ码 | 5060网一级毛片 | 久久99精品久久久久久国产越南 | 99re热精品视频国产免费 | 国产精品一区二区视频 | 免费一区二区三区在线视频 | 久久久久久蜜桃一区二区 | 亚洲视频国产 | 免费不卡一区 | 搞黄网站在线观看 | 日本亚洲欧美 | 久久99精品久久久久久青青日本 | 欧美日韩中文国产一区发布 | 国产专区视频 | 日韩爱爱网 | 欧美视频成人 | 日韩成人一区 | 国产一区免费 | 最近日韩中文字幕 | 亚洲三级视频 | 黄色日本视频 | 久久精品视频在线免费观看 | 国产亚洲精品精品国产亚洲综合 | 日韩成人一区 | 成人免费视频观看视频 | 久久精品亚洲精品国产欧美 | 毛片网站免费观看 | 亚洲人免费视频 | 国内精品视频在线 | wwwxx在线观看 | 激情影院久久 | 国产精品一区二区久久 | 亚洲高清在线观看 | 成人黄视频在线观看 | 亚洲情视频 | 一级毛片在线播放 | 黄色网页在线 |