您好,登錄后才能下訂單哦!
JavaScript 函數的閉包是怎樣的,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
前言
我們知道,作用域鏈查找標識符的順序是從當前作用域開始一級一級往上查找。因此,通過作用域鏈,JavaScript函數內部可以讀取函數外部的變,但反過來,函數的外部通常則無法讀取函數內部的變量。在實際應用中,有時需要真正在函數外部訪問函數內部的局部變量,此時最常用的方法就是使用閉包。
那么什么是閉包?所謂閉包,就是同時含有對函數對象以及作用域對象引用的對象。閉包主要是用來獲取作用域鏈或原型鏈上的變量或值。創建閉包最常見的方式是在一個函數中聲明內部函數(也稱嵌套函數),并返回內部函數。此時在函數外部就可以通過調用函數得到內部函數。雖然按照閉包的概念,所有訪問了外部變量的JavaScript函數都是閉包。但我們平常絕大部分時候所謂的閉包其實指的就是內部函數閉包。
閉包可以將一些數據封裝私有屬性以確保這些變量的安全訪問,這個功能給應用帶來了極大的好處。需要注意的是,閉包如果使用不當,也會帶來一些意想不到的問題。下面就通過幾個示例來演示一下閉包的創建、使用和可能存在的問題及其解決方法。
示例1: 創建閉包。
<!DOCTYPE html> <html> <head> <title>閉包</title> </head> <body> <script type="text/javascript"> function outer(argument) { var b=0; return function inner (){ b++; console.log("內部的b:"+b); } } var func = outer();//1 通過外部變量引用函數返回的內部函數 console.log(func);//2 輸出內部函數定義代碼 func();//3 通過閉包訪問局部變量b,此時b=1; console.log("外部函數中b:"+b); //4 出錯,報引用錯誤。 </script> </body> </html>
上述代碼在外部函數outer中聲明內部函數inner,并返回內部函數,同時在outer函數外面,變量func引用了outer函數返回的內部函數,所以內部函數inner是一個閉包。該閉包訪問了外部函數的局部變量b。1處代碼通過調用外部函數返回內部函數并賦給外部變量func,使func變量引用內部函數,所以2處代碼將輸出inner函數的整個定義代碼。3處代碼通過對外部變量func添加一對小括號后調用內部函數inner,從而達到在函數外部訪問局部變量b的目的。執行4處的代碼時將報ReferenceError錯誤,因為b是局部變量,不能在函數外部直接訪問局部變量。
我們知道函數執行完畢時,運行期上下文會被銷毀,與之關聯的活動對象也會隨之銷毀,因此離開函數后,屬于活動對象的局部變量將不能被訪問。但是為什么上述示例中的outer函數執行完后,它的局部變量還能被內部函數訪問呢?這個問題我們可以用作用域鏈來解釋。
當執行1處代碼調用outer函數時,JavaScript引擎會創建outer函數執行上下文的作用域鏈,這個作用域鏈包含了outer函數執行時的活動對象,同時JavaScript引擎也會創建一個閉包,而閉包因為需要訪問outer函數的局部變量,因而其作用鏈也會引用outer的活動對象。這樣,當outer函數執行完后,它的作用域對象因為有閉包的引用而依然存在,固而可以提供給閉包訪問。
上述示例中的內部函數雖然有名稱,但在調用是并沒有用到這個名稱,所以內部函數的名稱可以缺省,即可以將內部函數修改為匿名函數,從而簡化代碼。
示例2: 經典閉包問題
<!DOCTYPE html> <html> <head> <title>經典閉包問題</title> <script type="text/javascript"> window.onload=function () { var abtn = document.getElementsByTagName("button"); for (var i = 0; i<abtn.length; i++) { abtn[i].onclick=function(){ alert("按鈕"+(i+1)); } } } </script> </head> <body> <button>按鈕1</button> <button>按鈕2</button> <button>按鈕3</button> </body> </html>
該示例期望實現的功能是,單擊每個按鈕時,在彈出的警告對話框中顯示相應的標簽內容,即單擊3個按鈕時將分別顯示“按鈕1”、“按鈕2”、“按鈕3”。
上述示例頁面加載完后觸發窗口加載事件,從而執行外層匿名函數,外層匿名函數執行完循環語句后使活動對象中的局部變量i的值修改為3。外層匿名函數執行完后撤銷,但由于其活動對象中的abtn和i變量被內層匿名函數引用,因而外層匿名函數的活動對象仍然存在堆中供內層匿名函數訪問。每執行一次循環都將創建一個閉包,這些閉包都引用了外層匿名函數的活動對象,因而訪問變量i時都得到3,這樣最后的結果是單擊每個按鈕,在警告對話框中顯示的文字都是“按鈕4” (i+1=3+1),與期望的功能不一致。造成這個問題的原因是,每個閉包都引用一個變量,如果我們使不同的閉包引用不同的變量,就可以實現輸出的結果不一樣。這個需求可使用多種方法實現,在此介紹使用立即調用函數表達式(IIFE)和ES6中的let創建塊即變量的方法。
IIFE指的是:在定義函數的時候直接執行,即此時函數定義變成了一個函數調用的語句。要讓一個函數定義語句變成函數調用語句,就需要將定義語句變為一個函數表達式,然后在該表達式后面再加一對圓括號()即可。將函數定義語句變為一個函數表達式的最常用方法就是將整個定義語句放在一對圓括號中。
1、IIFE中的函數為一個匿名函數
(function(name){ console.log("hello,"+name); })("maomin");
JS引擎執行上述代碼時,會調用匿名,同時將后面圓括號中的參數maomin傳給name虛參,結果得到:"hello,maomin"。
2、IIFE中的函數為一個有名函數
(function func (name) { console.log("I am"+name); })("maomin")
上述代碼跟匿名函數完全一樣。
示例3: 使用立即調用函數表達式解決經典閉包問題
<!DOCTYPE html> <html> <head> <title>使用立即調用表達式解決經典閉包問題</title> <script type="text/javascript"> window.onload=function () { var abtn = document.getElementsByTagName("button"); for (var i = 0; i<abtn.length; i++) { (function(num){ abtn[num].onclick=function(){ alert("按鈕"+(num+1)); } })(i) } } </script> </head> <body> <button>按鈕1</button> <button>按鈕2</button> <button>按鈕3</button> </body> </html>
上述代碼中第二個匿名函數為IIFE,每次調用該匿名函數時將生成一個對應該函數的活動對象。該對象中包含可一個函數參數,值為當次循環的循環變量值。上述示例中,IIFE共執行了3次,因而共生成了3個活動對象,活動對象中包含的參數值分別為0、1和2,依次對應IIFE的3次執行。
每次執行IIFE時,將會產生一個閉包,該閉包會引用對應按鈕索引順序執行IIFE的活動對象,而閉包引用的活動對象中的參數值剛好等于按鈕的索引值,因而單擊3個按鈕將在彈出的警告框中分別顯示"按鈕1"、“按鈕2”、“按鈕3”。
示例4:使用ES6中的let關鍵字創建塊級變量解決經典閉包問題
<!DOCTYPE html> <html> <head> <title>使用ES6中的let關鍵字解決經典閉包問題</title> <script type="text/javascript"> window.onload=function () { var abtn = document.getElementsByTagName("button"); for (let i = 0; i<abtn.length; i++) { abtn[i].onclick=function(){ alert("按鈕"+(i+1)); } } } </script> </head> <body> <button>按鈕1</button> <button>按鈕2</button> <button>按鈕3</button> </body> </html>
上述代碼中循環變量使用let聲明,因而每次循環時,都會產生一個新的塊級變量,所以在頁面加載完,執行外層匿名函數時產生的活動對象中包含了3個對應循環變量的塊級變量,變量值分為0、1和2。每執行一次循環,將會產生一個閉包,該閉包中的變量i會引用外層匿名函數的活動對象對應按鈕索引的塊級變量,因而單擊3個按鈕時將在彈出的警告對話框中分別顯示“按鈕1”、“按鈕2”、“按鈕3”。
關于JavaScript 函數的閉包是怎樣的問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。