js知識點之閉包

閉包

什么是閉包

閉包,是 JavaScript 中一個非常重要的知識點,也是我們前端面試中較高幾率被問到的知識點之一。

打開《JavaScript 高級程序設計》和《 JavaScript 權威指南》,會發現里面針對閉包的解釋各執一詞,在網絡上搜索關于閉包的內容,也發現眾說紛紜,這就導致了這個知識點本身顯得有點神秘,甚至還有一點玄幻。

那么這個知識點真的有那么深奧么?

非也!其實要理解 JavaScript 中的閉包,非常容易,但是在此之前你需要先知道以下兩個知識點:

  • JavaScript 中的作用域和作用域鏈
  • JavaScript 中的垃圾回收

這里我們來簡單回顧一下這兩個知識點:

1. JavaScript 中的作用域和作用域鏈

  • 作用域就是一個獨立的地盤,讓變量不會外泄、暴露出去,不同作用域下同名變量不會有沖突。
  • 作用域在定義時就確定,并且不會改變。
  • 如果在當前作用域中沒有查到值,就會向上級作用域去查,直到查到全局作用域,這么一個查找過程形成的鏈條就叫做作用域鏈。

2. JavaScript 中的垃圾回收

  • Javascript 執行環境會負責管理代碼執行過程中使用的內存,其中就涉及到一個垃圾回收機制
  • 垃圾收集器會定期(周期性)找出那些不再繼續使用的變量,只要該變量不再使用了,就會被垃圾收集器回收,然后釋放其內存。如果該變量還在使用,那么就不會被回收。

OK,有了這 2 個知識點的鋪墊后,接下來我們再來看什么是閉包。

閉包不是一個具體的技術,而是一種現象,是指在定義函數時,周圍環境中的信息可以在函數中使用。換句話說,執行函數時,只要在函數中使用了外部的數據,就創建了閉包。

而作用域鏈,正是實現閉包的手段。

什么?只要在函數中使用了外部的數據,就創建了閉包?

真的是這樣么?下面我們可以證明一下:

image-20211227145016552

在上面的代碼中,我們在函數 a 中定義了一個變量 i,然后打印這個 i 變量。對于 a 這個函數來講,自己的函數作用域中存在 i 這個變量,所以我們在調試時可以看到 Local 中存在變量 i

下面我們將上面的代碼稍作修改,如下圖:

image-20211227145521272

在上面的代碼中,我們將聲明 i 這個變量的動作放到了 a 函數外面,也就是說 a 函數在自己的作用域已經找不到這個 i 變量了,它會怎么辦?

學習了作用域鏈的你肯定知道,它會順著作用域鏈一層一層往外找。然而上面在介紹閉包時說過,如果出現了這種情況,也就是函數使用了外部的數據的情況,就會創建閉包。

仔細觀察調試區域,我們會發現此時的 i 就放在 Closure 里面的,從而證實了我們前面的說法。

那么是一個函數下所有的變量聲明都會被放入到閉包這個封閉的空間里面么?

倒也不是,放不放入到閉包中,要看其他地方有沒有對這個變量進行引用,例如:

image-20211227164333723

在上面的代碼中,函數 c 中一個變量都沒有創建,卻要打印 i、j、kx,這些變量分別存在于 a、b 函數以及全局作用域中,因此創建了 3 個閉包,全局閉包里面存儲了 i 的值,閉包 a 中存儲了變量 jk 的值,閉包 b 中存儲了變量 x 的值。

但是你仔細觀察,你就會發現函數 b 中的 y 變量并沒有被放在閉包中,所以要不要放入閉包取決于該變量有沒有被引用。

當然,此時的你可能會有這樣的一個新問題,那么多閉包,那豈不是占用內存空間么?

實際上,如果是自動形成的閉包,是會被銷毀掉的。例如:

image-20211227174043786

在上面的代碼中,我們在第 16 行嘗試打印輸出變量 k,顯然這個時候是會報錯的,在第 16 行打一個斷點調試就可以清楚的看到,此時已經沒有任何閉包存在,垃圾回收器會自動回收沒有引用的變量,不會有任何內存占用的情況。

當然,這里我指的是自動產生閉包的情況,關于閉包,有時我們需要根據需求手動的來制造一個閉包。

來看下面的例子:

function eat(){var food = "雞翅";console.log(food);
}
eat(); // 雞翅
console.log(food); // 報錯

在上面的例子中,我們聲明了一個名為 eat 的函數,并對它進行調用。

JavaScript 引擎會創建一個 eat 函數的執行上下文,其中聲明 food 變量并賦值。

當該方法執行完后,上下文被銷毀,food 變量也會跟著消失。這是因為 food 變量屬于 eat 函數的局部變量,它作用于 eat 函數中,會隨著 eat 的執行上下文創建而創建,銷毀而銷毀。所以當我們再次打印 food 變量時,就會報錯,告訴我們該變量不存在。

但是我們將此代碼稍作修改:

function eat(){var food = '雞翅';return function(){console.log(food);}
}
var look = eat();
look(); // 雞翅
look(); // 雞翅

在這個例子中,eat 函數返回一個函數,并在這個內部函數中訪問 food 這個局部變量。調用 eat 函數并將結果賦給 look 變量,這個 look 指向了 eat 函數中的內部函數,然后調用它,最終輸出 food 的值。

為什么能訪問到 food,原因很簡單,上面我們說過,垃圾回收器只會回收沒有被引用到的變量,但是一旦一個變量還被引用著的,垃圾回收器就不會回收此變量。在上面的示例中,照理說 eat 調用完畢 food 就應該被銷毀掉,但是我們向外部返回了 eat 內部的匿名函數,而這個匿名函數有引用了 food,所以垃圾回收器是不會對其進行回收的,這也是為什么在外面調用這個匿名函數時,仍然能夠打印出 food 變量的值。

至此,閉包的一個優點或者特點也就體現出來了,那就是:

  • 通過閉包可以讓外部環境訪問到函數內部的局部變量。
  • 通過閉包可以讓局部變量持續保存下來,不隨著它的上下文環境一起銷毀。

通過此特性,我們可以解決一個全局變量污染的問題。早期在 JavaScript 還無法進行模塊化的時候,在多人協作時,如果定義過多的全局變量 有可能造成全局變量命名沖突,使用閉包來解決功能對變量的調用將變量寫到一個獨立的空間里面,從而能夠一定程度上解決全局變量污染的問題。

例如:

var name = "GlobalName";
// 全局變量
var init = (function () {var name = "initName";function callName() {console.log(name);// 打印 name}return function () {callName();// 形成接口}
}());
init(); // initName
var initSuper = (function () {var name = "initSuperName";function callName() {console.log(name);// 打印 name}return function () {callName();// 形成接口}
}());
initSuper(); // initSuperName

好了,在此小節的最后,我們來對閉包做一個小小的總結:

  • 閉包是一個封閉的空間,里面存儲了在其他地方會引用到的該作用域的值,在 JavaScript 中是通過作用域鏈來實現的閉包。

  • 只要在函數中使用了外部的數據,就創建了閉包,這種情況下所創建的閉包,我們在編碼時是不需要去關心的。

  • 我們還可以通過一些手段手動創建閉包,從而讓外部環境訪問到函數內部的局部變量,讓局部變量持續保存下來,不隨著它的上下文環境一起銷毀。

閉包經典問題

聊完了閉包,接下來我們來看一個閉包的經典問題。

for (var i = 1; i <= 3; i++) {setTimeout(function () {console.log(i);}, 1000);
}

在上面的代碼中,我們預期的結果是過 1 秒后分別輸出 i 變量的值為 1,2,3。但是,執行的結果是:4,4,4

實際上,問題就出在閉包身上。你看,循環中的 setTimeout 訪問了它的外部變量 i,形成閉包。

i 變量只有 1 個,所以循環 3 次的 setTimeout 中都訪問的是同一個變量。循環到第 4 次,i 變量增加到 4,不滿足循環條件,循環結束,代碼執行完后上下文結束。但是,那 3setTimeout1 秒鐘后才執行,由于閉包的原因,所以它們仍然能訪問到變量 i,不過此時 i 變量值已經是 4 了。

要解決這個問題,我們可以讓 setTimeout 中的匿名函數不再訪問外部變量,而是訪問自己內部的變量,如下:

for (var i = 1; i <= 3; i++) {(function (index) {setTimeout(function () {console.log(index);}, 1000);})(i)
}

這樣 setTimeout 中就可以不用訪問 for 循環聲明的變量 i 了。而是采用調用函數傳參的方式把變量 i 的值傳給了 setTimeout,這樣它們就不再創建閉包,因為在我自己的作用域里面能夠找到 i 這個變量。

當然,解決這個問題還有個更簡單的方法,就是使用 ES6 中的 let 關鍵字。

它聲明的變量有塊作用域,如果將它放在循環中,那么每次循環都會有一個新的變量 i,這樣即使有閉包也沒問題,因為每個閉包保存的都是不同的 i 變量,那么剛才的問題也就迎刃而解。

for (let i = 1; i <= 3; i++) {setTimeout(function () {console.log(i);}, 1000);
}

真題解答

  • 閉包是什么?閉包的應用場景有哪些?怎么銷毀閉包?

閉包是一個封閉的空間,里面存儲了在其他地方會引用到的該作用域的值,在 JavaScript 中是通過作用域鏈來實現的閉包。

只要在函數中使用了外部的數據,就創建了閉包,這種情況下所創建的閉包,我們在編碼時是不需要去關心的。

我們還可以通過一些手段手動創建閉包,從而讓外部環境訪問到函數內部的局部變量,讓局部變量持續保存下來,不隨著它的上下文環境一起銷毀。

使用閉包可以解決一個全局變量污染的問題。

如果是自動產生的閉包,我們無需操心閉包的銷毀,而如果是手動創建的閉包,可以把被引用的變量設置為 null,即手動清除變量,這樣下次 JavaScript 垃圾回收器在進行垃圾回收時,發現此變量已經沒有任何引用了,就會把設為 null 的量給回收了。


-EOF-

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/19056.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/19056.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/19056.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Java中如何指定jdk的版本運行jar包

你的jdk安裝的目錄\bin\java -jar 你的jar包名字.jar 這是我的代碼示例 C:\Users\86177\.jdks\corretto-17.0.10\bin\java -jar big_event_study2-0.0.1- SNAPSHOT.jar

23種設計模式之一— — — —裝飾模式詳細介紹與講解

裝飾模式詳細講解 一、定義二、裝飾模式結構核心思想模式角色模式的UML類圖應用場景模式優點模式缺點 實例演示圖示代碼演示運行結果 一、定義 裝飾模式&#xff08;別名&#xff1a;包裝器&#xff09; 裝飾模式&#xff08;Decorator Pattern&#xff09;是結構型的設計模式…

LeetCode 每日一題 數學篇 2651.計算列車到站時間

給你一個正整數 arrivalTime 表示列車正點到站的時間&#xff08;單位&#xff1a;小時&#xff09;&#xff0c;另給你一個正整數 delayedTime 表示列車延誤的小時數。 返回列車實際到站的時間。 注意&#xff0c;該問題中的時間采用 24 小時制。 int findDelayedArrivalTi…

學業輔導導師:文心一言智能體詳細介紹和開發

一、前言 本期題目 開發方向&#xff1a;學習成長類 解讀&#xff1a; AI技術在學習成長方向的應用正日益增多&#xff0c;本期賽題需圍繞該方向開發智能體包括但不限于:作文輔導助手、個性化學習助手、考試助手、各垂類教育內容專家等 二、我的智能體&#xff1a;學業輔導…

macbook中foxmail的通訊錄遷移

之前windows中用習慣了foxmail,換成macbook后,還是沿用foxmail。使用一段時間后,確實受不了foxmail的不便:1、版本比較低1.5.6,很多windows新版的功能都沒有;2、動不動莫名其妙崩潰,寫了半天的郵件,點擊發送就直接崩了,又得重新寫。 忍耐了幾個月后,下定決心換成網易…

2.10 mysql設置遠程訪問權限

2.10 mysql設置遠程訪問權限 目錄1. 管理員運行mysql命令窗口2. 使用 root 用戶重新登錄 MySQL3. 修改用戶權限4. 修改mysql安裝目錄下的my.ini 目錄 說明&#xff1a; Mysql8.0 設置遠程訪問權限 一、Mysql8.0 設置遠程訪問權限 1. 管理員運行mysql命令窗口 2. 使用 root 用…

matlab安裝及破解

一、如何下載 軟件下載鏈接&#xff0c;密碼&#xff1a;98ai 本來我想自己生成一個永久百度網盤鏈接的&#xff0c;但是&#xff1a; 等不住了&#xff0c;所以大家就用上面的鏈接吧。 二、下載花絮 百度網盤下載速度比上載速度還慢&#xff0c;我給充了個會員&#xff0c…

【1】:計算機圖形學概述

從技術角度講&#xff0c;什么是好的畫面呢&#xff1f; 看這個畫面是不是足夠亮&#xff0c;也就是全局光照做的夠好 什么是計算機圖形學? 使用計算機合成和操作可視信息。 應用場景 Video Games 游戲 Movie 電影 Animation 動畫 Design 設計&#xff1a;CAD等軟件相關…

修復CentOS 6.6服務器YUM和RPM功能異常的技術實踐20240523

修復CentOS 6.6服務器YUM和RPM功能異常的技術實踐 引言 在復雜的生產環境中&#xff0c;服務器的穩定性至關重要。近期&#xff0c;我們遇到了一臺CentOS 6.6服務器在執行yum update -y時被中斷&#xff0c;導致YUM和RPM功能異常的問題。本文將詳細介紹問題的診斷、解決過程以及…

java中變量名單獨占用一個空間嗎,為什么能直接使用變量名而不需要給java地址,變量名和地址之間有什么關系

在 Java 中&#xff0c;變量名不單獨占用存儲空間&#xff0c;但它們確實在內存中有對應的地址。為了理解這一點&#xff0c;我們需要深入了解變量名和內存地址之間的關系。 變量名與內存地址 變量名的作用: 在 Java 程序中&#xff0c;變量名是用于引用存儲在內存中的數據的…

git顯示提交次數

git shortlog 是一個特殊版本的 git log 命令&#xff0c;旨在創建發布公告。它將每個提交按作者分組&#xff0c;并顯示每個提交消息的第一行。這是一種快速查看不同作者在項目中的貢獻的方式。 以下是 git shortlog 的一些常用參數&#xff1a; -n 或 --numbered&#xff1…

Java多線程——Lambda表達式

λ希臘字母表中排序第十一位的字母&#xff0c;英語名稱為Lambda&#xff1b; 避免匿名內部類定義過多&#xff1b; 其實質屬于函數式編程的概念。 為什么要用Lambda表達式&#xff1f; 1. 避免匿名內部類定義過多&#xff1b; 2. 可以讓你的代碼看起來更簡潔&#xff1b; …

OpenAI 文生圖模型演進:DDPM、IDDPM、ADM、GLIDE、DALL-E 2、DALL-E 3

節前&#xff0c;我們星球組織了一場算法崗技術&面試討論會&#xff0c;邀請了一些互聯網大廠朋友、參加社招和校招面試的同學。 針對算法崗技術趨勢、大模型落地項目經驗分享、新手如何入門算法崗、該如何準備、面試常考點分享等熱門話題進行了深入的討論。 合集&#x…

WPF使用Prism實現簡單訂餐系統

新建wpf項目&#xff0c;nuget引入Prism.DryIoc&#xff0c;MaterialDesignThemes 引入后&#xff0c;修改App.xaml 前臺引入 xmlns:prism"http://prismlibrary.com/"和prism:PrismApplication App.xaml.cs App.xaml.cs繼承PrismApplication&#xff0c;重寫CreateS…

在線等!3damx渲染爆內存怎么辦?

在使用V-Ray進行CPU渲染時&#xff0c;復雜場景和高渲染設置可能會導致內存消耗過高&#xff0c;進而影響渲染速度&#xff0c;導致處理異常、機器停滯、應用程序崩潰等情況。 為機器配置更大的 RAM 始終是解決問題的最有效辦法&#xff0c;但如果出于預算等原因無法實現&…

Lua的幾個特殊用法

&#xff1a;/.的區別 詳細可以參考https://zhuanlan.zhihu.com/p/651619116。最重要的不同就是傳遞默認參數self。 通過.調用函數&#xff0c;傳遞self實例 通過 &#xff1a; 調用函數&#xff0c;傳遞self (不需要顯示的傳遞self參數&#xff0c;默認就會傳遞&#xff0c;但…

調出idea解決沖突界面

背景 我對idea使用不熟練&#xff0c;還是習慣用git bash來合并代碼&#xff0c;合并爆沖突后&#xff0c;我進入idea準備解決沖突&#xff0c;卻發現找不到解決沖突的界面。 解決 右擊idea中沖突的文件&#xff0c;將鼠標移動到菜單欄的git上&#xff0c;此時應該出現包含有…

Leecode熱題100--二分查找---33:搜索旋轉排序矩陣

題目&#xff1a; 整數數組 nums 按升序排列&#xff0c;數組中的值 互不相同 。 給你 旋轉后 的數組 nums 和一個整數 target &#xff0c;如果 nums 中存在這個目標值 target &#xff0c;則返回它的下標&#xff0c;否則返回 -1 。 思路&#xff1a; 此處采用容易理解的兩次…

端口掃描利器--nmap

目錄 普通掃描 幾種指定目標的方法 TCP/UDP掃描 端口服務掃描 綜合掃描 普通掃描 基于端口連接并響應(真實) ? nmap -sn 網段(0/24)-sn 幾種指定目標的方法 單個IP掃描 IP范圍掃描 掃描文件里的IP 掃描網段,(排除某IP) 掃描網段(排除某清單IP) TCP/UDP掃描 -sS …

linux中邏輯卷管理與擴展

邏輯卷管理與擴展 邏輯卷 作用&#xff1a; 1.整合分散的空間2.空間支持擴大 邏輯卷制作過程&#xff1a;將眾多的物理卷&#xff08;PV&#xff09;組建成卷組&#xff08;VG&#xff09;&#xff0c;再從卷組中劃分出邏輯卷&#xff08;LV&#xff09; 邏輯卷的邏輯思路 …