首頁 > web前端 > js教程 > 主體

JavaScript總結分享之閉包

WBOY
發布: 2022-11-07 16:31:08
轉載
1251 人瀏覽過

本篇文章為大家帶來了關於JavaScript的相關知識,其中主要介紹了關於閉包的相關問題,其中包括了閉包是什麼、為什麼要這麼設計以及能怎麼用的相關內容,下面一起來看一下,希望對大家有幫助。

JavaScript總結分享之閉包

【相關推薦:JavaScript影片教學web前端

閉包是什麼?

對於一個知識點來說,我一直認為不論是從什麼方面入手,都需要徹底弄懂三個問題,才算真正了解這個知識點,然後具體再去實踐中練習,才能稱得上掌握。這三個問題就是:

  • 是什麼?
  • 為什麼要設計?
  • 能用在哪?

先回答閉包是什麼這個問題。應該大多數人也看過很多與之相關的文章,很多人也給了自己的解釋,所以我也先給出自己理解的解釋,那就是: 先有兩個前置的概念:

  • 閉包是在詞法分析時就已經被確定的, 所以它會與詞法作用域有關。

  • 閉包存在的前置條件是需要支援函數作為一等公民的程式語言,所以它會與函數有關。

所以最終的結論是:

  • 閉包首先是一個結構體,這個結構體的組成部分為 一個函數該函數所處的詞法作用域
  • 也就是閉包是由一個函數並且函數能夠記住宣告自己的詞法作用域所產生的結構體
  • 在記憶體中理解就是, 當函數被呼叫時,它所產生的函數執行上下文裡的作用域鏈保存有其父詞法作用域,所以父變數物件由於存在而被引用而不會銷毀,駐留在記憶體中供其使用。這樣的情況就稱為閉包。

上述的解釋對於已經了解過閉包的人應該是一目了然的,但其實如果對於一個完全不知曉閉包的人來說,很可能是完全看不懂的。更甚至很多人其實只是記住了這個定義,而不是真的理解了這內涵。

所以我想用一個不一定精準的類比去幫助理解什麼是閉包這東西,想像你寫了一篇文章放在自己的伺服器上,並且引用了自己的3篇文章作為參考。那麼此時 一篇文章 伺服器的環境 就類似閉包。

在發佈到網路上後被轉載到其他的平台上,而其他平台上的讀者點開你的文章閱讀後想繼續看你所引用的那些文章,就被準確無誤的跳轉到了你伺服器裡的文章中去。

在這個範例中,這篇文章保存了寫這篇文章的伺服器環境裡的參考。 因而不論在哪裡讀到文章,文章裡所記得的參考文章引用指向永遠是伺服器裡的位址。這種情況叫做使用了閉包的特性。

可能例子還是不太好理解,畢竟它也沒有很準確,閉包這概念就是有點抽象,沒有想到現實中有什麼具體的例子可以用來比喻。如果有人想出更好的類比可以指出,我加以註解和描述。

為什麼要設計出閉包?

對於為什麼設計這一點,僅以我自己粗淺的理解就是由於JavaScript是非同步單執行緒的語言。對於非同步程式設計來說,最大的問題就是當你編寫了函數,而等到它真正呼叫的時機可能是之後任意的時間節點。

這對記憶體管理來說是一個很大的問題,正常同步執行的程式碼,函數宣告時和被呼叫時所需要的資料都還存留在記憶體中,可以無障礙的獲取。而異步的程式碼,往往聲明該函數的上下文可能已經銷毀,等到在呼叫它時,如果記憶體中已經把它所需要的一些外部資料給清理了,這就是個很大的問題。

所以JavaScript解決的方案就是讓函數能夠記得自己之前所能獲取資料的範圍,統統都保存在記憶體裡,只要函數沒有被記憶體回收,它本身以及所能記住的範圍都不會被銷毀

這裡的所能記住的範圍就是指詞法作用域,就是由於它是靜態的,所以才需要記住。

這又是JavaScript設計作用域為靜態的導致的。如果是動態作用域,函數被呼叫時只需要被呼叫時的那個環境,就不需要存在記住自身作用域的事了。

所以總結一下就是:

  • 閉包是為了解決詞法作用域引發的問題記憶體不好管理非同步程式設計裡資料獲取所產生的。

經典題

原本我的想法是從最底層來解釋閉包的情況,後來在查閱各種文章時發現, 有一篇文章已經寫的很好了。那就是JavaScript閉包的底層運作機制, 我覺得可以先看看這篇的講解然後在看我之後所寫的內容。

由於有非常多的文章都從下面這個非常經典的面試題入手,但似乎都沒有人真正從最底層講解過,所以我就打算將整個過程梳理一遍,來明白這其中的差異性。

for (var i = 0; i < 3; i++) {  setTimeout(function cb() {    console.log(i);
  }, 1000);
}
登入後複製

基本上所有有基礎的人一眼就能看出輸出的是三個3。

然後讓修改成依序輸出,通常只需要修改var成let:

for (let i = 0; i < 3; i++) {  setTimeout(function cb() {    console.log(i);
  }, 1000);
}
登入後複製

這樣就成了輸出為0,1,2.並且是同時間輸出,而不是每間隔一秒輸出一次。

那麼問題來了,為什麼?

這裡可以先不看下面,先寫寫自己的解釋,看看是否跟我寫的一樣。

1. 先來探討變數i是var的情況。

當程式碼開始執行時,此時執行上下文堆疊和記憶體裡的情況是這樣: 其中全域物件裡的變數i和全域執行上下文裡變數環境裡的變數i是同一個變數。

然後開始進行循環, 當i = 0時,第一個計時器被丟入巨集任務佇列,關於巨集任務相關的內容屬於事件循環範疇,暫時只需要理解setTimeout會被丟入佇列裡,等之後執行。 此時在堆記憶體中會建立它的回呼函數cb,且函數在建立時會建立[[scope]],在實際ECMA的規則中,[[scope]]會指向函數的父作用域,也就是當前的全域物件(作用域是概念上的東西,實際體現在記憶體中就是保存資料的一種結構,可能是物件也可能是其他)。 但在V8引擎的實作中,其實並不會指向全域對象,而是去分析該函數使用了父作用域中的哪些變量,將這些變數儲存到Closure中,然後由scope指向。每個函數都有且只有一個Closure物件。


這裡先插入關於Closure物件可以在Chrome中哪看到的情況: 可以看到,在建立bar函數時,它只有引用了父作用域的name變量,所以在閉包物件中只會儲存變數name, 而不會存在變數age。


同理之後的i = 1, 和i = 2 都是一樣的,最終結果會變成:

最終因為i 導致i = 3, 迴圈結束,全域程式碼執行完畢。此時的結果為:

然後開始進入定時器回呼函數執行的過程, 開始執行第一個定時器裡的回調函數,壓入了執行上下文堆疊中,執行輸出i, 但是在詞法環境和變數環境中找不到這個變數i,所以去自身[[scope]]向上尋找,在Closure物件中找到了i 等於3,輸出結果3。

同理對於後面兩個計時器也是一樣的流程,並且實際上定時器開啟的時間都是在循環中就立即執行的,導致實際上三個函數的定時1秒時間是一致的,最終輸出的結果幾乎同時輸出3個3。而不是每間隔1秒後輸出3, 當然這是定時器相關的知識了。

2. 接著探討透過var修改成let之後實際上變了什麼

同樣是剛建立時,所展示的情況為:

#之後進入循環體,當i = 0時:

#之後進入i = 1時的情況:

最後進入到i = 2的情況,與i = 1基本上類似:

最終i ,變成i值為3 ,循環結束。開啟定時器工作:

当执行第一个定时器的回调函数时,创建了函数执行上下文,此时执行输出语句i时,会先从自己的词法环境里寻找变量i的值,也就是在 record环境记录里搜索,但是不存在。因而通过自己外部环境引用outer找到原先创建的块级作用域里 i = 0的情况, 输出了i值为0的结果。

对于之后的定时器也都是一样的情况,原先的块级作用域由于被回调函数所引用到了,因而就产生了闭包的情况,不会在内存中被销毁,而是一直留着。

等到它们都执行完毕后,最终内存回收会将之全部都销毁。

其实以上画的图并不是很严谨,与实际在内存中的表现肯定是有差异的,但是对于理解闭包在内存里的情况还是不影响的。

闭包能用在哪?

首先需要先明确一点,那就是在JavaScript中,只要创建了函数,其实就产生了闭包。这是广义上的闭包,因为在全局作用域下声明的函数,也会记着全局作用域。而不是只有在函数内部声明的函数才叫做闭包。

通常意义上所讨论的闭包,是使用了闭包的特性

1. 函数作为返回值

let a = 1function outer() {  let a = 2

  function inside() {
    a += 1
    console.log(a)
  }  return inside
}const foo = outer()foo()
登入後複製

此处outer函数调用完时,返回了一个inside函数,在执行上下文栈中表示的既是outer函数执行上下文被销毁,但有一个返回值是一个函数。 该函数在内存中创建了一个空间,其[[scope]]指向着outer函数的作用域。因而outer函数的环境不会被销毁。

当foo函数开始调用时,调用的就是inside函数,所以它在执行时,先询问自身作用域是否存在变量a, 不存在则向上询问自己的父作用域outer,存在变量a且值为2,最终输出3。

2. 函数作为参数

var name = &#39;xavier&#39;function foo() {  var name = &#39;parker&#39;
  function bar() {    console.log(name)
  } console.log(name)  return bar
}function baz(fn) {  var name = &#39;coin&#39;
  fn()
}baz(foo())baz(foo)
登入後複製

对于第一个baz函数调用,输出的结果为两个'parker'。 对于第二个baz函数的调用,输出为一个'parker'。

具体的理解其实跟上面一致,只要函数被其他函数调用,都会存在闭包。

3. 私有属性

闭包可以实现对于一些属性的隐藏,外部只能获取到属性,但是无法对属性进行操作。

function foo(name) {  let _name = name  return {    get: function() {      return _name
    }
  }
}let obj = foo(&#39;xavier&#39;)
obj.get()
登入後複製

4. 高阶函数,科里化,节流防抖等

对于一些需要存在状态的函数,都是使用到了闭包的特性。

// 节流function throttle(fn, timeout) {  let timer = null
  return function (...arg) {    if(timer) return
    timer = setTimeout(() => {
    fn.apply(this, arg)
    timer = null
    }, timeout)
  }
}// 防抖function debounce(fn, timeout){  let timer = null
  return function(...arg){    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, arg)
    }, timeout)
  }
}
登入後複製

5. 模块化

在没有模块之前,对于不同地方声明的变量,可能会产生冲突。而闭包能够创造出一个封闭的私有空间,为模块化提供了可能性。 可以使用IIFE+闭包实现模块。

var moduleA = (function (global, doc) {  var methodA = function() {};  var dataA = {};  return {    methodA: methodA,    dataA: dataA
  };
})(this, document);
登入後複製

【相关推荐:JavaScript视频教程web前端

以上是JavaScript總結分享之閉包的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:juejin.im
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!