最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

從來(lái)都沒(méi)有理解JavaScript閉包? 今天非把你教會(huì)不可! 看這一篇就夠了,全程大白話(huà)!

2022-02-21 07:50 作者:極客小俊GeekerJun  | 我要投稿

極客小俊

一個(gè)把邏輯思維轉(zhuǎn)變?yōu)榇a的技術(shù)博主



前言

這么多年了,你是否還在討論javascript閉包呢? 閉包這個(gè)概念幾乎也是任何前端面試官都會(huì)必考的問(wèn)題!

并且理解javascript閉包也是邁向高級(jí)前端開(kāi)發(fā)工程師的必經(jīng)之路!

也只有理解了閉包原理和運(yùn)行機(jī)制才能寫(xiě)出更為安全和優(yōu)雅的javascript代碼

那么你是否學(xué)習(xí)javascript很久了但閉包還沒(méi)有搞懂呢? ?? 閉包很晦澀難懂嗎? ?或許你把閉包這個(gè)概念想象得太過(guò)神奇! ?今天就來(lái)揭秘javascript閉包 一個(gè)前端開(kāi)發(fā)經(jīng)久不衰的話(huà)題!


學(xué)習(xí)條件

這里我也特別說(shuō)明一下閉包其實(shí)牽扯的東西還是有點(diǎn)多,涉及到以下JS知識(shí)點(diǎn):

  1. 函數(shù)的執(zhí)行上下文環(huán)境(Execution context of function)

  2. 變量對(duì)象(Variable object)

  3. 活動(dòng)對(duì)象(Active object)

  4. 作用域(scope)

  5. 作用域鏈(scope chain)

那么如果你對(duì)以上所涉及到的知識(shí)點(diǎn)還沒(méi)有清楚,那么建議補(bǔ)一下,我以后也會(huì)慢慢提及, 否則理解閉包就會(huì)出現(xiàn)歧義!


到底什么是閉包?

概述

閉包比較書(shū)面化的解釋是: 一個(gè)擁有許多變量和綁定了這些變量的環(huán)境的表達(dá)式,并且通常是一個(gè)函數(shù), 而這些變量也是該表達(dá)式的一部分。我想如果你是一個(gè)零基礎(chǔ)的小白, 那么估計(jì)不出意外的話(huà)應(yīng)該完全不能理解這句話(huà)!?? 沒(méi)關(guān)系想搞懂我們接著往下看...

那么我們首先來(lái)看一段JS代碼

?//函數(shù)定義
?function outerTest() {
? ? ?var num = 0;
? ? ?function innerTest() {
? ? ? ? ?++num
? ? ? ? ?console.log(num);
? ? ?}
? ? ?return innerTest;
?}
?
?//調(diào)用
?var fn1 = outerTest();
?fn1();
?fn1();
?fn1();

運(yùn)行結(jié)果

在這里插入圖片描述

以上就是一個(gè)閉包的經(jīng)典案例, 我們慢慢來(lái)分析!

其實(shí)你會(huì)發(fā)現(xiàn)以上這段JS代碼有兩個(gè)特點(diǎn):

1、innerTest函數(shù)嵌套在outerTest函數(shù)的內(nèi)部

2、outerTest函數(shù)的返回值就是innerTest函數(shù)

那么有人就會(huì)說(shuō)函數(shù)嵌套函數(shù)就是閉包 其實(shí)這樣子說(shuō)是不嚴(yán)謹(jǐn)?shù)?


原理分析

接著之前的那一段JS代碼 我們來(lái)看一張圖

在這里插入圖片描述

代碼分析

當(dāng)在執(zhí)行完var fn1 = outerTest();之后,變量fn1實(shí)際上是指向了函數(shù)innerTest

那么接下來(lái)如果再執(zhí)行fn1()就會(huì)改變num變量的值, 當(dāng)然這個(gè)過(guò)程通常懂一點(diǎn)程序執(zhí)行流程也可以分析出來(lái)!

關(guān)鍵不同的是之后繼續(xù)執(zhí)行fn1()輸出的卻是num變量累加之后的結(jié)果! 你肯定想知道為什么會(huì)累加!對(duì)吧!??

首先因?yàn)?code>函數(shù)innerTest引用了函數(shù)outerTest內(nèi)部的變量或者數(shù)據(jù),再然后重點(diǎn)來(lái)了:

當(dāng)一個(gè)局部函數(shù)或匿名函數(shù)被定義的時(shí)候,那么它的作用域鏈也會(huì)被初始化,并且雖然有的時(shí)候局部函數(shù)即便是沒(méi)有被調(diào)用,但是它會(huì)執(zhí)行一個(gè)動(dòng)作: 就是復(fù)制一份父函數(shù)的作用域鏈, 并且再將此作用域鏈的第0位插入該未調(diào)用函數(shù)的變量對(duì)象,等到該函數(shù)被調(diào)用了就激活為活動(dòng)對(duì)象

如果實(shí)在你還無(wú)法理解這里的【作用域鏈】,那么你可以理解為是一種描述路徑的術(shù)語(yǔ), 沿著該路徑可以找到需要的變量值!

再次回到閉包的概念上來(lái), 也就是當(dāng)一個(gè)子函數(shù)引用了父級(jí)函數(shù)的某個(gè)變量或數(shù)據(jù),那么 閉包其實(shí)就產(chǎn)生了

并且這個(gè)變量或數(shù)據(jù)的生命周期始終能保持使用,就能間接保持原構(gòu)父級(jí)函數(shù) 在內(nèi)存中的變量對(duì)象不會(huì)消失

所以盡管outerTest()函數(shù)已經(jīng)調(diào)用結(jié)束, 但是子函數(shù)卻始終能引用到該父級(jí)函數(shù)中的變量的值,并且該變量值只能通這種方法來(lái)訪(fǎng)問(wèn)!

即使再次調(diào)用相同的outerTest()函數(shù),但只會(huì)生成相對(duì)應(yīng)的變量對(duì)象,新的變量對(duì)象只是對(duì)應(yīng)新的值, 和上次那次調(diào)用的是各自獨(dú)立的!

如圖

在這里插入圖片描述

簡(jiǎn)而言之 在嵌套在父級(jí)函數(shù)內(nèi)部的子函數(shù)被定義時(shí),并且也引用了父級(jí)函數(shù)的數(shù)據(jù)時(shí)就產(chǎn)生了閉包

需要重點(diǎn)注意的是: 一個(gè)閉包內(nèi)對(duì)變量的修改,不會(huì)影響到另外一個(gè)閉包中的變量

以上案例就是在outerTest函數(shù)執(zhí)行完并返回后,閉包使得JS中的的垃圾回收機(jī)制GC(Garbage collection)不會(huì)收回outerTest函數(shù)所占用的資源,這里指的資源是它的變量對(duì)象, 因?yàn)?code>outerTest函數(shù)的內(nèi)部函數(shù)innerTest的執(zhí)行一直需要依賴(lài)outerTest函數(shù)中的變量或者其他數(shù)據(jù)。這就是對(duì)閉包產(chǎn)生和特性最直白通俗的描述!

那么現(xiàn)在回過(guò)頭來(lái)再次理解為什么每次調(diào)用fn1()函數(shù) 變量num會(huì)累加? 看下面這張圖!

如圖

在這里插入圖片描述

因?yàn)橛捎?code>閉包的存在使得函數(shù)outerTest返回后,函數(shù)outerTest中的num變量其實(shí)始終存在與內(nèi)存中,這樣每次執(zhí)行fn1(),都會(huì)找到內(nèi)存中與之對(duì)應(yīng)outerTest函數(shù)變量對(duì)象num變量進(jìn)行累加1后,輸出num的值


閉包具體步驟總結(jié)

  1. 當(dāng)執(zhí)行函數(shù)outerTest的時(shí)候,outerTest函數(shù)會(huì)進(jìn)入相應(yīng)的執(zhí)行上下文環(huán)境!

  2. 在創(chuàng)建函數(shù)outerTest執(zhí)行環(huán)境的過(guò)程中,首先會(huì)為函數(shù)outerTest添加一個(gè)scope屬性,即函數(shù)outerTest的作用域,其值就為函數(shù)outerTest中的作用域鏈scope chain

  3. 然后執(zhí)行環(huán)境會(huì)創(chuàng)建一個(gè)活動(dòng)對(duì)象(activation object)。活動(dòng)對(duì)象也是當(dāng)前被調(diào)用這個(gè)函數(shù)所擁有的一個(gè)對(duì)象,它是用來(lái)保存數(shù)據(jù)的, 它不能通過(guò)JS代碼直接訪(fǎng)問(wèn), (如果你實(shí)在理解不了可以想象成一個(gè)抽象的對(duì)象)

  4. 創(chuàng)建完活動(dòng)對(duì)象后,把該活動(dòng)對(duì)象添加到outerTest函數(shù)作用域鏈中的最頂端,也就是圖中的第0位,此時(shí)outerTest函數(shù)作用域鏈包含了兩個(gè)對(duì)象:outerTest函數(shù)活動(dòng)對(duì)象全局window變量對(duì)象也就是圖中藍(lán)色和綠色兩個(gè)對(duì)象

  5. 然后在outerTest函數(shù)活動(dòng)對(duì)象上添加一個(gè)arguments屬性,它保存著調(diào)用outerTest函數(shù)時(shí)所傳遞的實(shí)際參數(shù),當(dāng)然我們這里并沒(méi)有傳遞任何參數(shù)進(jìn)來(lái)!

  6. 再然后把所有outerTest函數(shù)形參和內(nèi)部的innerTest函數(shù)、以及num變量這些數(shù)據(jù)的引用也添加到outerTest函數(shù)活動(dòng)對(duì)象上。

  7. 此時(shí)完成了函數(shù)innerTest的定義,因此如同第3步,函數(shù)innerTest作用域鏈以及innerTest函數(shù)的變量對(duì)象跟之前outerTest函數(shù)一樣被初始化了, ?那么到這里整個(gè)outerTest函數(shù)從定義到執(zhí)行的步驟就完成了!

  8. 然后在外部 outerTest函數(shù)返回innerTest函數(shù)命名為fn1引用變量,又因?yàn)?code>innerTest函數(shù)的作用域鏈包含了對(duì)outerTest函數(shù)變量對(duì)象的引用,注意:此時(shí)outerTest函數(shù)已經(jīng)調(diào)用結(jié)束,活動(dòng)對(duì)象也變成了內(nèi)存中滯留的變量對(duì)象,那么innerTest函數(shù)可以訪(fǎng)問(wèn)到outerTest函數(shù)中定義的所有變量和函數(shù), 并且innerTest函數(shù)被外部的fn1所引用,函數(shù)innerTest又依賴(lài)函數(shù)outerTest,因此函數(shù)outerTest變量對(duì)象在返回后不會(huì)被JS垃圾回收機(jī)制GC(Garbage collection)銷(xiāo)毀。


所以當(dāng)fn1執(zhí)行也相當(dāng)于在執(zhí)行函數(shù)innerTest時(shí)候也會(huì)像以上步驟一樣。因此執(zhí)行時(shí)innerTest函數(shù)作用域鏈包中含了3個(gè)對(duì)象:innerTest函數(shù)活動(dòng)對(duì)象、outerTest函數(shù)變量對(duì)象全局window變量對(duì)象, 也就是圖中藍(lán)色+綠色+紫色三個(gè)對(duì)象, 如果你覺(jué)得上圖看不清楚那么就看下面這張圖!

如圖

在這里插入圖片描述

當(dāng)在innerTest函數(shù)中訪(fǎng)問(wèn)一個(gè)變量時(shí),搜索順序是先搜索自身的活動(dòng)對(duì)象如果存在則返回

注意: 如果函數(shù)innerTest存在prototype原型對(duì)象,則在查找完自身的活動(dòng)對(duì)象后, 會(huì)先查找自身的原型對(duì)象

如果不存在將繼續(xù)搜索滯留在內(nèi)存中outerTest函數(shù)變量對(duì)象,依次查找直到找到為止, 這就是JS中的數(shù)據(jù)查找機(jī)制 ,當(dāng)然如果整個(gè)作用域鏈上都無(wú)法找到,則返回undefined

我們?cè)诶斫忾]包的時(shí)候 重點(diǎn)也是在作用域鏈這個(gè)環(huán)節(jié)容易出錯(cuò), 要知道函數(shù)的定義與執(zhí)行的區(qū)別。

函數(shù)作用域是在函數(shù)定義時(shí)就已經(jīng)確定,而不是在執(zhí)行的時(shí)候確定, 這里引出了一個(gè)概念詞法作用域

舉個(gè)栗子??

?function outer(num) {
? ?function inner() {
? ? ? ?return num;
? ? }
? ?return inner;
?}
?var fn1 = outer(1);
?console.log(fn1());

我們假設(shè)函數(shù)fn1作用域是在執(zhí)行時(shí),也就是console.log(fn1())確定的,那么此時(shí)fn1的作用域鏈?zhǔn)侨缦拢?/p>

函數(shù)fn1的活動(dòng)對(duì)象->console.log的活動(dòng)對(duì)象->window對(duì)象,如果假設(shè)成立,那么輸出值就必然是undefined

另一種假設(shè)也就是函數(shù)fn1的作用域是在定義時(shí)確定的,就是說(shuō)fn1指向的inner函數(shù)在定義的時(shí)候就已經(jīng)確定了作用域。那么在執(zhí)行的時(shí)候,函數(shù)fn1的作用域鏈為如下:

函數(shù)fn1的活動(dòng)對(duì)象->函數(shù)outer的變量對(duì)象->window對(duì)象,如果假設(shè)成立,那么輸出值也就是1。

所以運(yùn)行結(jié)果最終為1,說(shuō)明了第2種假設(shè)是正確的,也就證明了函數(shù)的作用域確實(shí)是在定義這個(gè)函數(shù)的時(shí)候就已經(jīng)確定了這個(gè)說(shuō)法!



有人又會(huì)問(wèn)如果我們不返回outerTest函數(shù)行不行呢? 答案肯定是不行的

因?yàn)?code>outerTest函數(shù)執(zhí)行完后,innerTest函數(shù)沒(méi)有被返回給外界,只是被outerTest函數(shù)所使用

因此函數(shù)outerTest函數(shù)innerTest互相使用, 但又不被外界使用,那么函數(shù)outerTest執(zhí)行完畢之后就會(huì)被GC(Garbage collection)垃圾回收機(jī)制回收, 那么outerTest函數(shù)執(zhí)行上下文環(huán)境也會(huì)被彈出call Stack, 內(nèi)存中也不會(huì)在有outerTest函數(shù)所對(duì)應(yīng)的變量對(duì)象了, 自然也無(wú)法繼續(xù)保存值了!

在這里插入圖片描述



閉包的應(yīng)用場(chǎng)景

應(yīng)用場(chǎng)景1 代碼模塊化

閉包的應(yīng)用場(chǎng)景主要是用于模塊化

閉包可以一定程度上保護(hù)函數(shù)內(nèi)的變量安全。

還是剛才的案例舉例!

outerTest函數(shù)中的num變量只有innerTest函數(shù)才能訪(fǎng)問(wèn),而無(wú)法通過(guò)其他途徑訪(fǎng)問(wèn)到,因此保護(hù)了num變量的安全性, 所以閉包模塊化基本可以解決函數(shù)污染變量隨意被修改問(wèn)題!


比如說(shuō)Java、php等語(yǔ)言中有支持將方法聲明為私有,它們只能被同一個(gè)類(lèi)中的其它方法所調(diào)用。

js是沒(méi)有這種原生支持的,但我們可以使用閉包來(lái)模擬私有方法。

私有方法不僅僅有利于限制對(duì)代碼的訪(fǎng)問(wèn)權(quán)限, 還提供了管理全局命名空間的強(qiáng)大能力,避免非核心的方法弄亂了代碼的公共接口部分。

舉個(gè)栗子??

var Counter = (function() {

?var privateCounter = 0;
?function changeBy(val) {
? privateCounter += val;
?}
?return {
?increment: function() {
? changeBy(1);
?},
?decrement: function() {
? changeBy(-1);
?},
?value: function() {
? return privateCounter;
?}
?}
?})();
?
?console.log(Counter.value()); /* 輸出 0 */
?Counter.increment(); ?//執(zhí)行遞增
?Counter.increment(); ?//執(zhí)行遞增
?console.log(Counter.value()); /* 輸出 2 */
?Counter.decrement(); ? //執(zhí)行遞減
?console.log(Counter.value()); /* 輸出 1 */

如圖

在這里插入圖片描述

以上案例表現(xiàn)了如何使用閉包來(lái)定義公共函數(shù),并讓它可以訪(fǎng)問(wèn)私有函數(shù)變量

IIFE匿名函數(shù)包含兩個(gè)私有數(shù)據(jù):名為 privateCounter 變量changeBy函數(shù), 而這兩項(xiàng)都無(wú)法在這個(gè)匿名函數(shù)外部直接訪(fǎng)問(wèn)。必須通過(guò)匿名函數(shù)返回的三個(gè)公共函數(shù)接口來(lái)進(jìn)行訪(fǎng)問(wèn)!

increment()、decrement()、value()這三個(gè)公共函數(shù)是共享同一個(gè)作用域執(zhí)行上下文環(huán)境的變量對(duì)象, 也就是閉包也多虧 js作用域,它們都可以訪(fǎng)問(wèn) privateCounter變量changeBy函數(shù)



應(yīng)用場(chǎng)景2 在內(nèi)存中保持變量數(shù)據(jù)一直不丟失!

還是以最開(kāi)始的例子, 由于閉包的影響,函數(shù)outerTestnum變量會(huì)一直存在于內(nèi)存中,因此每次執(zhí)行外部的fn1()時(shí),都會(huì)給num變量進(jìn)行累加!

所以每累加一次也就是每調(diào)用一次fn1() 就會(huì)去內(nèi)存中一層層尋找outerTest函數(shù)變量對(duì)象里面的num進(jìn)行累加!


現(xiàn)在完全明白了閉包了吧!??

如果你真的理解了閉包,那么下面這個(gè)案例就很容易去推理了,也非常經(jīng)典 就是在事件循環(huán)中如何保留每一次循環(huán)的索引值!

代碼栗子

html代碼

<button>Button0</button>

<button>Button1</button>

?<button>Button2</button>

?<button>Button3</button>

?<button>Button4</button>

js代碼

?window.onload=function(){
?var btns = document.getElementsByTagName('button');
? for(var i = 0,len = btns.length; i < len; i++) {
? ? ?btns[i].onclick = function() {
? ? ? ? ?console.log(i);
?? ? }
? ?}
?}


分析

通過(guò)執(zhí)行該段代碼,其實(shí)你會(huì)發(fā)現(xiàn)不論點(diǎn)擊哪個(gè)button按鈕 ,均輸出5,

如圖

在這里插入圖片描述



這是很多初學(xué)者 或者還沒(méi)有完全理解閉包的朋友心中的困惑! ??? ?那今天就要跟你解開(kāi)這個(gè)困惑了!

首先你要明白一點(diǎn), onclick事件是被異步觸發(fā)的,也就是等著用戶(hù)事件被觸發(fā)時(shí),for循環(huán)其實(shí)早已結(jié)束!

此時(shí)變量 i 的值已經(jīng)是5 ?所以當(dāng)onlick事件函數(shù)順著作用域鏈從內(nèi)向外查找變量 i時(shí),找到的值總是 5

也就是這個(gè)變量i已經(jīng)在外層的變量對(duì)象中一直保存的都是最終值!

如果你想要每次都打印出所 對(duì)應(yīng)的索引號(hào) 這里就要使用到閉包了!

修改js代碼如下形式

?window.onload=function(){
? ? ?var btns = document.getElementsByTagName('button');
? ? ?for(var i = 0, len = btns.length; i < len; i++) {
? ? ? ? ?(function(i) {
? ? ? ? ? ? ?btns[i].onclick = function() {
? ? ? ? ? ? ? ? ?console.log(i);
? ? ? ? ? ? ?}
? ? ? ? ?}(i))
? ? ?}
?}

或者

?window.onload=function(){
? ? ?var btns = document.getElementsByTagName('button');
? ? ?for(var i = 0, len = btns.length; i < len; i++) {
? ? ? ? ?function test(index){
? ? ? ? ? ? ?btns[index].onclick = function() {
? ? ? ? ? ? ? ? ?console.log(index);
? ? ? ? ? ? ?}
? ? ? ? ?}
? ? ? ? ?test(i)
? ? ?}
?}

這樣一來(lái)每次循環(huán)的變量i值都被封閉起來(lái),這樣在事件函數(shù)執(zhí)行時(shí),會(huì)查找定義時(shí)的作用域鏈,這個(gè)作用域鏈里的變量i值是在每次循環(huán)中都被保留在對(duì)應(yīng)的變量對(duì)象中,因此點(diǎn)擊不同的button按鈕會(huì)輸出不同的變量i

如圖

在這里插入圖片描述



閉包的缺陷

如果不是某些特定業(yè)務(wù)需求下, 盡量避免使用閉包,因?yàn)?code>閉包在處理速度和內(nèi)存消耗方面對(duì)腳本性能具有負(fù)面影響, 其會(huì)根據(jù)閉包數(shù)量的多少而在內(nèi)存中創(chuàng)建更多的變量對(duì)象, 最終可能會(huì)導(dǎo)致內(nèi)存溢出 等情況!

當(dāng)然通常最簡(jiǎn)單的解決辦法就是: 解除對(duì)引用變量函數(shù)的使用

?引用變量函數(shù) = null;

我們可以將引用變量的值將其設(shè)置為null即可,js垃圾回收將會(huì)將其清除, 釋放內(nèi)存資源!

總結(jié)閉包

1、當(dāng)內(nèi)部函數(shù) 在定義它的作用域的外部被引用(使用)時(shí),就創(chuàng)建了該內(nèi)部函數(shù)的閉包 ,如果內(nèi)部函數(shù)引用了位于父級(jí)函數(shù)的變量或者其他數(shù)據(jù)時(shí),當(dāng)父級(jí)函數(shù)調(diào)用完畢后,這些變量數(shù)據(jù)在內(nèi)存不會(huì)被GC(Garbage collection)釋放,因?yàn)?code>閉包它們被一直引用著!否則兩者沒(méi)有交互就不會(huì)長(zhǎng)久存在于內(nèi)存中,所以在Chrome中的debug找不到閉包

2、通過(guò)調(diào)用閉包的內(nèi)部函數(shù)獲取到閉包的成員變量: 在閉包中返回該函數(shù),在外部接收該函數(shù)并執(zhí)行就能獲取閉包的成員變量。 原因是因?yàn)?code>詞法作用域,也就是函數(shù)的作用域是其聲明的作用域而不是執(zhí)行調(diào)用時(shí)的作用域



大家的支持就是我堅(jiān)持下去的動(dòng)力!


從來(lái)都沒(méi)有理解JavaScript閉包? 今天非把你教會(huì)不可! 看這一篇就夠了,全程大白話(huà)!的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
西安市| 江西省| 宜君县| 昌宁县| 彝良县| 古浪县| 雅江县| 思茅市| 玉田县| 永清县| 凤城市| 长阳| 宝山区| 昌图县| 武川县| 德惠市| 定结县| 越西县| 崇仁县| 开江县| 高陵县| 壶关县| 苏州市| 崇阳县| 泰和县| 甘洛县| 桃江县| 望奎县| 阜新市| 襄城县| 甘孜| 灵石县| 青冈县| 宁乡县| 长治县| 榆树市| 浦城县| 梁山县| 舟曲县| 临邑县| 阳朔县|