溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JS中為什么有閉包

發(fā)布時間:2021-10-29 09:55:59 來源:億速云 閱讀:253 作者:iii 欄目:web開發(fā)

本篇內(nèi)容主要講解“JS中為什么有閉包”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“JS中為什么有閉包”吧!

太長不看版

當(dāng)「函數(shù)訪問在其外部定義的變量時」,你需要閉包。

例如,這段代碼包含一個閉包:

let users = ['Alice', 'Dan', 'Jessica']; let query = 'A'; let user = users.filter(user => user.startsWith(query));

注意 user => user.startsWith(query) 本身是一個函數(shù)。它使用了 query 變量。但是,query  變量是在該函數(shù)的“外部”定義的。那就是閉包。

「如果你愿意,可以在這里就停止閱讀」。本文的其余部分會以不同的方式去處理閉包,并不解釋閉包是什么,而是帶你完成「發(fā)現(xiàn)」閉包的過程——就像1960年代的第一批程序員所做的那樣。

第1步:函數(shù)可以訪問外部變量

要了解閉包,我們需要對變量和函數(shù)有所了解。在這個例子中,我們在 eat 函數(shù)中聲明了 food 變量。

function eat() {   let food = 'cheese';   console.log(food + ' is good'); }  eat(); // => 'cheese is good'

但是,如果我們以后想更改 eat 函數(shù)的 food 變量,該怎么辦?為此,我們可以將 food 變量本身從函數(shù)中移到頂層:

let food = 'cheese'; // 我們把它移動到外部  function eat() {   console.log(food + ' is good'); }

這樣我們可以在任何有需要的時候“從外部” 修改 food:

eat(); // => 'cheese is good' food = 'pizza'; eat(); // => 'pizza is good' food = 'sushi'; eat(); // => 'sushi is good'

換句話說,food 變量不再是 eat 函數(shù)的局部變量,但是 eat  函數(shù)仍然可以輕松訪問它。「函數(shù)可以訪問它們之外的變量」。先停下來想一秒鐘,確保你對這個想法沒有任何疑問。然后繼續(xù)執(zhí)行第二步。

第2步:在函數(shù)調(diào)用中包裝代碼

假設(shè)我們有一些代碼:

/* 一些代碼片段 */

這些代碼做什么無關(guān)緊要。但是,假設(shè)「我們要運行兩次」。

一種方法是復(fù)制并粘貼:

/* 一些代碼片段 */ /* 一些代碼片段 */

另一種方法是使用循環(huán):

for (let i = 0; i < 2; i++) {   /* 一些代碼片段 */ }

第三種方法,也是我們今天特別感興趣的一種方法,將其包裝在一個函數(shù)中:

function doTheThing() {   /* 一些代碼片段 */ }  doTheThing(); doTheThing();

函數(shù)為我們提供了很大的靈活性,因為我們可以隨時在程序中的任何位置把這個函數(shù)執(zhí)行任意次。

如果愿意,「我們也可以只調(diào)用一次」:

function doTheThing() {   /* 一些代碼片段 */ }  doTheThing();

請注意,上面的代碼與原始代碼段是等效的:

/* 一些代碼片段 */

換句話說,「如果我們有一段代碼,將代碼“包裝”到一個函數(shù)中,然后只調(diào)用一次,那么我們就不會改變代碼的作用」。我們會忽略此規(guī)則的一些例外,但總的來說這應(yīng)該是有道理的。停留在這個想法上,直到你的大腦完全理解為止。

第3步:發(fā)現(xiàn)閉包

前面我們通過兩種不同的想法進行了探索:

  • 函數(shù)可以訪問在其外部定義的變量。

  • 在函數(shù)中包裝代碼并調(diào)用一次不會改變結(jié)果。

那么如果把它們結(jié)合在一起會發(fā)生些什么呢。

我們將從第一步的代碼開始:

let food = 'cheese';  function eat() {   console.log(food + ' is good'); }  eat();

然后,將整個例子中的代碼包裝到一個函數(shù)中,該函數(shù)將被調(diào)用一次:

function liveADay() {   let food = 'cheese';    function eat() {     console.log(food + ' is good');   }    eat(); }  liveADay();

再次審視兩個代碼片段,并確保它們是等效的。

這段代碼有效!但是仔細看,注意 eat 函數(shù)在 liveADay 函數(shù)的內(nèi)部。這允許嗎?我們真的可以將一個函數(shù)放在另一個函數(shù)中嗎?

在某些語言中,用這種方式寫出來的代碼是「無效」的。例如這種代碼在 C 語言(沒有閉包)中無效。這意味著在 C  語言中,前面的第二個結(jié)論是不正確的&mdash;&mdash;我們不能隨隨便便就把一些代碼包裝在函數(shù)中。但是 JavaScript 不受這種限制。

再看這段代碼,并注意在哪里聲明和使用了 food:

function liveADay() {   let food = 'cheese'; // 聲明 `food`    function eat() {     console.log(food + ' is good'); // 使用 `food`   }    eat(); }  liveADay();

讓我們一起逐步看一下這段代碼。首先在頂層聲明 liveADay 函數(shù),然后立即調(diào)用它。它有一個 food 局部變量,還包含一個 eat 函數(shù)。然后調(diào)用  eat 功能。因為 eat在 liveADay 內(nèi)部,所以它“看到”了所有變量。這就是為什么它可以讀取 food 變量的原因。

「這就是閉包」。

「我們說當(dāng)函數(shù)(例如 eat)讀取或?qū)懭朐谄渫獠?例如在 food 中)聲明的變量(例如 food)時,存在閉包?!?/p>

花一些時間多讀幾遍,并確保你已經(jīng)理解了上面的代碼代碼。

下面是本文最開始介紹過的例子:

let users = ['Alice', 'Dan', 'Jessica']; let query = 'A'; let user = users.filter(user => user.startsWith(query));

如果用函數(shù)表達式重寫,則更容易注意到閉包:

let users = ['Alice', 'Dan', 'Jessica']; // 1. 查詢變量在外部聲明 let query = 'A'; let user = users.filter(function(user) {   // 2. 我們處于嵌套函數(shù)中   // 3. 然后我們讀取查詢變量(在外部聲明?。?nbsp;  return user.startsWith(query); });

每當(dāng)函數(shù)訪問在其外部聲明的變量時,我們就說它是一個閉包。這個術(shù)語本身在使用時有些寬松。在本例中,有些人把「嵌套函數(shù)本身」稱為“閉包”。其他人可能會把訪問外部變量的“技術(shù)”稱為閉包。實際上這都沒關(guān)系。

函數(shù)調(diào)用的幽靈

閉看似簡單,但是這并不意味著他們沒有自己的陷阱。如果你真正考慮一下,函數(shù)可以在外部讀取和寫入變量的事實將會產(chǎn)生深遠的影響。這意味著只要可以調(diào)用嵌套函數(shù),這些變量就會“存活”下去:

function liveADay() {   let food = 'cheese';    function eat() {     console.log(food + ' is good');   }    // Call eat after five seconds   setTimeout(eat, 5000); }  liveADay();

在這里,food 是在 liveADay() 函數(shù)調(diào)用內(nèi)的局部變量。在我們退出 liveADay  之后,很容易想到它“消失了”,并且它不會回來困擾我們。

但是,在 liveADay 內(nèi)部,我們告訴瀏覽器在五秒鐘內(nèi)調(diào)用 eat。然后,eat 讀取 food 變量?!敢虼耍琂avaScript引擎需要使特定的  liveADay() 調(diào)用中的food變量保持可用,直到調(diào)用eat。」

從這種意義上講,我們可以將閉包視為過去函數(shù)調(diào)用的“幻象”或“內(nèi)存”。即使我們的 liveADay() 函數(shù)調(diào)用已經(jīng)完成很長時間,但只要仍可以調(diào)用嵌套的  eat 函數(shù),那么它的變量就必須繼續(xù)存在。幸運的是,JavaScript 為我們做到了這一點,因此我們就無需再去考慮它了。

為什么會有“閉包”?

最后,你可能想知道為什么以這種方式調(diào)用閉包。主要是歷史原因。一位熟悉計算機科學(xué)術(shù)語的人可能會說像 user =>  user.startsWith(query) 之類的表達式具有“開放綁定”。換句話說,從中可以清楚地知道 user 是什么(一個參數(shù)),但是還不能確定  query 是孤立的。當(dāng)我們說“實際上,query 指的是在外部聲明的變量”時,我們是在“關(guān)閉”開放綁定。換句話說,我們得到一個 閉包。

并非所有語言都實現(xiàn)閉包。例如在一些像 C  這樣的語言中,根本不允許嵌套函數(shù)。結(jié)果,一個函數(shù)只能訪問自己的局部變量或全局變量,永遠不會出現(xiàn)訪問父函數(shù)的局部變量的情況。當(dāng)然,這種限制是痛苦的。

還有像 Rust 這樣的語言,它們實現(xiàn)了閉包,但是對于閉包和常規(guī)函數(shù)有著單獨的語法。因此,如果你想從函數(shù)外部讀取變量,則必須在 Rust  中選擇使用該變量。這是因為在底層,即使在函數(shù)調(diào)用之后,閉包也可能要求引擎保持外部變量(稱為“環(huán)境”)。這種開銷在 JavaScript  中是可以接受的,但是對于非常低級的語言來說,則可能會引發(fā)性能方面的問題。

到此,相信大家對“JS中為什么有閉包”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

js
AI