天天看點

重學JavaScript(函數)閉包

學習javascript切勿好高骛遠。正所謂貪多嚼不爛,前端标準和工具這幾年的飛速發展,以及不時冒出的“新鮮玩意”讓衆多前端從業者驚呼:“學不動啦學不動啦!學習速度跟不上技術發展速度!我感到手忙腳亂、力不從心……"如果你有以上“症狀”,請勿着急,這不過是你内心不安造成的。你為何追新?你又何苦追新?在根基不牢的情況下,就算蓋樓蓋到18層,再往上堆一塊磚,都可能導緻大樓坍塌!這結果絕非你預期。是以,此時你應該沉下心來苦練基礎。而非死鑽牛角尖。硬要及時掌握那些業界最新冒出來的“玩意兒”對你無益處。

我們知道,作用域鍊查找辨別符的順序是從目前作用域開始一級一級往上查找。是以,通過作用域鍊,javascript函數内部可以讀取函數外部的變,但反過來,函數的外部通常則無法讀取函數内部的變量。在實際應用中,有時需要真正在函數外部通路函數内部的局部變量,此時最常用的方法就是使用閉包。

那麼什麼是閉包?所謂閉包,就是同時含有對函數對象以及作用域對象引用的對象。閉包主要是用來擷取作用域鍊或原型鍊上的變量或值。建立閉包最常見的方式是在一個函數中聲明内部函數(也稱嵌套函數),并傳回内部函數。此時在函數外部就可以通過調用函數得到内部函數。雖然按照閉包的概念,所有通路了外部變量的javascript函數都是閉包。但我們平常絕大部分時候所謂的閉包其實指的就是内部函數閉包。

閉包可以将一些資料封裝私有屬性以確定這些變量的安全通路,這個功能給應用帶來了極大的好處。需要注意的是,閉包如果使用不當,也會帶來一些意想不到的問題。下面就通過幾個示例來示範一下閉包的建立、使用和可能存在的問題及其解決方法。

示例1: 建立閉包。

上述代碼在外部函數<code>outer</code>中聲明内部函數<code>inner</code>,并傳回内部函數,同時在<code>outer</code>函數外面,變量<code>func</code>引用了<code>outer</code>函數傳回的内部函數,是以内部函數<code>inner</code>是一個閉包。該閉包通路了外部函數的局部變量<code>b</code>。1處代碼通過調用外部函數傳回内部函數并賦給外部變量<code>func</code>,使<code>func</code>變量引用内部函數,是以2處代碼将輸出<code>inner</code>函數的整個定義代碼。3處代碼通過對外部變量<code>func</code>添加一對小括号後調用内部函數<code>inner</code>,進而達到在函數外部通路局部變量<code>b</code>的目的。執行4處的代碼時将報<code>referenceerror</code>錯誤,因為<code>b</code>是局部變量,不能在函數外部直接通路局部變量。

我們知道函數執行完畢時,運作期上下文會被銷毀,與之關聯的活動對象也會随之銷毀,是以離開函數後,屬于活動對象的局部變量将不能被通路。但是為什麼上述示例中的<code>outer</code>函數執行完後,它的局部變量還能被内部函數通路呢?這個問題我們可以用作用域鍊來解釋。

當執行1處代碼調用<code>outer</code>函數時,javascript引擎會建立<code>outer</code>函數執行上下文的作用域鍊,這個作用域鍊包含了<code>outer</code>函數執行時的活動對象,同時javascript引擎也會建立一個閉包,而閉包因為需要通路<code>outer</code>函數的局部變量,因而其作用鍊也會引用<code>outer</code>的活動對象。這樣,當<code>outer</code>函數執行完後,它的作用域對象因為有閉包的引用而依然存在,固而可以提供給閉包通路。

上述示例中的内部函數雖然有名稱,但在調用是并沒有用到這個名稱,是以内部函數的名稱可以預設,即可以将内部函數修改為匿名函數,進而簡化代碼。

示例2: 經典閉包問題

該示例期望實作的功能是,單擊每個按鈕時,在彈出的警告對話框中顯示相應的标簽内容,即單擊3個按鈕時将分别顯示“按鈕1”、“按鈕2”、“按鈕3”。

上述示例頁面加載完後觸發視窗加載事件,進而執行外層匿名函數,外層匿名函數執行完循環語句後使活動對象中的局部變量i的值修改為<code>3</code>。外層匿名函數執行完後撤銷,但由于其活動對象中的<code>abtn</code>和<code>i</code>變量被内層匿名函數引用,因而外層匿名函數的活動對象仍然存在堆中供内層匿名函數通路。每執行一次循環都将建立一個閉包,這些閉包都引用了外層匿名函數的活動對象,因而通路變量i時都得到<code>3</code>,這樣最後的結果是單擊每個按鈕,在警告對話框中顯示的文字都是“按鈕4” <code>(i+1=3+1)</code>,與期望的功能不一緻。造成這個問題的原因是,每個閉包都引用一個變量,如果我們使不同的閉包引用不同的變量,就可以實作輸出的結果不一樣。這個需求可使用多種方法實作,在此介紹使用立即調用函數表達式(<code>iife</code>)和es6中的<code>let</code>建立塊即變量的方法。

<code>iife</code>指的是:在定義函數的時候直接執行,即此時函數定義變成了一個函數調用的語句。要讓一個函數定義語句變成函數調用語句,就需要将定義語句變為一個函數表達式,然後在該表達式後面再加一對圓括号()即可。将函數定義語句變為一個函數表達式的最常用方法就是将整個定義語句放在一對圓括号中。

1、iife中的函數為一個匿名函數

js引擎執行上述代碼時,會調用匿名,同時将後面圓括号中的參數<code>maomin</code>傳給<code>name</code>虛參,結果得到:“hello,maomin”。

2、iife中的函數為一個有名函數

上述代碼跟匿名函數完全一樣。

示例3: 使用立即調用函數表達式解決經典閉包問題

上述代碼中第二個匿名函數為<code>iife</code>,每次調用該匿名函數時将生成一個對應該函數的活動對象。該對象中包含可一個函數參數,值為當次循環的循環變量值。上述示例中,iife共執行了<code>3</code>次,因而共生成了<code>3</code>個活動對象,活動對象中包含的參數值分别為<code>0</code>、<code>1</code>和<code>2</code>,依次對應<code>iife</code>的<code>3</code>次執行。

每次執行<code>iife</code>時,将會産生一個閉包,該閉包會引用對應按鈕索引順序執行<code>iife</code>的活動對象,而閉包引用的活動對象中的參數值剛好等于按鈕的索引值,因而單擊<code>3</code>個按鈕将在彈出的警告框中分别顯示"按鈕1"、“按鈕2”、“按鈕3”。

示例4:使用es6中的let關鍵字建立塊級變量解決經典閉包問題

上述代碼中循環變量使用<code>let</code>聲明,因而每次循環時,都會産生一個新的塊級變量,是以在頁面加載完,執行外層匿名函數時産生的活動對象中包含了<code>3</code>個對應循環變量的塊級變量,變量值分為<code>0</code>、<code>1</code>和<code>2</code>。每執行一次循環,将會産生一個閉包,該閉包中的變量<code>i</code>會引用外層匿名函數的活動對象對應按鈕索引的塊級變量,因而單擊<code>3</code>個按鈕時将在彈出的警告對話框中分别顯示“按鈕1”、“按鈕2”、“按鈕3”。

下一期更精彩
歡迎關注我的公衆号,回複關鍵詞【電子書】,即可擷取近十幾本前端熱門電子書。更有精品文章等着你哦。 你還可以加我微信,我拉攏了很多it大佬,建立了一個技術交流、文章分享群,歡迎你的加入。 作者:vam的金豆之路 主要領域:前端開發 微信公衆号:前端曆劫之路
重學JavaScript(函數)閉包

繼續閱讀