詳細聊聊閉包在js中充當著什麼角色

什麼是閉包

開篇明義,概念先行。閉包是什麼,為什麼說在js中處處充滿瞭閉包。

閉包就是函數有權訪問另一個函數作用域中的變量,此函數和被引用的變量一起構成瞭閉包

文字描述文縐縐的難以理解,看一下代碼就能夠一目瞭然瞭

function test() {
    var a = 1
    var b = function() {
        console.log(a)
    }
    
    return b
}

在上面的代碼例子中,變量 a 處於函數 test 的作用域中,但是函數 b 中可以對變量 a 進行訪問。
套用閉包的概念,也就是函數 b 有權訪問函數 test 作用域中的變量 a,此時函數 b 與變量 a 就形成瞭一個閉包。

看瞭上面的例子,大傢是否恍然大悟,這不就是我們在代碼中經常寫的嗎。所以說js中處處充滿瞭閉包。

如何觀察閉包

如果一開始我們對於閉包的認識還不是很深刻,我們怎麼知道在代碼中寫出瞭一個閉包呢?一招教你找出閉包

function test() {
    let a = 1
    return function test1() {
        debugger
        console.log(a)
    }
}

test()()

如上的一段代碼,在執行到 debugger 關鍵字的時候,我們可以打開瀏覽器的開發者調制工具,此時我們可以從調用棧中看到 Closure 的字樣,這就是代表我們寫出瞭一個閉包啦

閉包的錯誤認識

說完瞭閉包的概念,再來說說可能大傢會對閉包產生的一些錯誤認識。

1.閉包的產生需要使用 return 暴露出去

首先從閉包的概念上來看就沒有說到閉包需要暴露到函數外才叫閉包,而是隻要引用瞭不屬於當前函數作用域中的變量就已經產生閉包瞭。

為什麼會有這樣的錯誤認識,是因為我們使用閉包引用瞭外部作用域中的變量,一般是為瞭將這個變量或者是這個函數暴露出去,讓我們在外部也可以訪問到這個變量或者函數,也就是說將閉包暴露到函數外部隻是我們的業務需求,而不是閉包的必要條件。

2.閉包會導致內存泄漏

首先我們要知道為什麼閉包會導致內存泄漏,是因為我們將閉包暴露到函數外部的時候,閉包內部仍然引用著其外部作用域中的變量,導致外部作用域中的變量無法被垃圾回收機制回收,如果循環引用閉包的話就容易造成內存泄漏的現象。但這是由於我們在使用閉包過程中所引起的,而不是閉包本身的性質所決定的,因此說閉包一定會導致內存泄漏是不嚴謹的。

(另外在 IE9 之後也對瀏覽器的垃圾回收機制做瞭優化,現在已經不容易導致內存泄露瞭)

閉包導致的問題

作為 js 中八大陷阱之一的循環陷阱,就是由於閉包引起的

for (var i = 0; i < 4; i++) {
    setTimeout(() => {
        console.log(i)
    }, 1000)
}  // 4, 4, 4, 4

執行以上代碼,會發現 1s 之後打印瞭 4個 4,為什麼不是打印 0, 1, 2, 3,就是因為 setTimeout 中的回調函數是一個閉包,引用瞭外部作用域中的 i 變量,但是 i 隻有一個,並不會在每個回調中生成新的 i,因此在 1s 後打印的時候訪問的是同一個作用域中的 i 變量,因此打印的結果就是 4個 4

如何解決以上問題,有兩個方法:

  • 一種是使用 es6 的 let 語法生成塊級作用域,這樣每個塊級作用域中的 i 變量就不是指向同一個 i 變量,不會對彼此產生影響
for (let i = 0; i < 4; i++) {
    setTimeout(() => {
        console.log(i)
    }, 1000)
}  // 0, 1, 2, 3
  • 一種是使用立即執行函數,每個立即執行函數中的變量 i 都是當前外部變量 i 的一個快照
for (let i = 0; i < 4; i++) {
    (function(i) {
        setTimeout(() => {
            console.log(i)
        }, 1000)
    })(i)
}  // 0, 1, 2, 3

閉包的使用場景

說瞭這麼多閉包的性質,甚至閉包還會引發循環陷阱這麼重大的問題,那麼閉包到底有什麼用?面試官問到的時候總不能說 js 處處都是閉包,所以 js 到處都是閉包的使用場景吧。那麼我們就來說說閉包的幾個經典使用場景

1. 單例模式

var CreateSingleton = (function() {
    var instance = null
    var CreateSingleton = function() {
        if (instance) return instance
        return instance = this
    }
    return CreateSingleton
})()

單例模式是設計模式的一種,目的是為瞭保證全局中隻有一個實例對象,上述代碼利用 instance 創建一個閉包。單例模式在組件庫保證全局中隻有一個彈窗組件尤其好用。

2. 函數柯裡化

柯裡化是將一個多參數的函數轉化成幾個單參數的函數嵌套的形式,例如: function test(a, b, c) => function test(a)(b)(c)

function currying(fn, args) {
  var _this = this
  var len = fn.length
  var args = args || []

  return function() {
    var _args = Array.prototype.slice.call(arguments)
    Array.prototype.push.apply(args, _args)
    
    if(_args.length < len) {
      return currying.call(this, fn, _args)
    }

    return fn.apply(this, _args)
  }
}

3. 與立即執行函數配合使用完成類庫的封裝

閉包往往配合著立即執行函數來一起使用,能夠發揮出強大的效果。因此,往往很多人容易被誤導,認為閉包與立即執行函數之間有什麼關系,甚至認為立即執行函數就是閉包。但這種認識其實是錯誤的,立即執行函數與閉包之間沒有任何關系。

在 jQuery 盛行的年代,各類規范百花爭艷,其中 umd 規范能夠兼容多種環境,主要在於其 umd 頭部結構的實現

(function( global, factory ) {
    "use strict";
    if ( typeof module === "object" && typeof module.exports === "object" ) {
        module.exports = global.document ?
        factory( global, true ) :
	function( w ) {
            if ( !w.document ) {
		throw new Error( "jQuery requires a window with a document" );
            }
            return factory( w );
	};
    } else {
	factory( global );
    }
})( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {})

以上這種代碼的寫法使用過 jQuery 開發的人應該非常熟悉吧。

4. 保存私有變量

在實際開發過程中,我們有時候需要對於計算結果進行緩存,或者是保存私有變量而不被外部訪問到,就可以使用閉包來實現。

另外,在目前流行的兩大前端框架 Vue 和 React 中其實也大量用到瞭閉包進行相關功能的實現,具體大傢可以自己翻翻源碼啦~

總結

到此這篇關於閉包在js中充當著什麼角色的文章就介紹到這瞭,更多相關js中的閉包內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: