一、 前言
第六彈內容是閉包。 距離上次函數的發布已經過去了一個多月, 最近事情比較多,很少有時間去寫文章, 低質量還得保證所以本章放草稿箱一個月了,終于補齊了,其實還有很多細節要展開說明,想著拖太久了,還是先發出來吧,后續慢慢補充。感謝各位佬的支持,希望多多點贊收藏,如果發現有什么不好的點也可以評論或私信我。
本系列為一周一更,計劃歷時6個月左右。從JS最基礎【變量與作用域】到【異步編程,密碼學與混淆】。希望自己能堅持下來, 也希望給準備入行JS逆向的朋友一些幫助, 我現在臉皮厚度還行。先要點贊,評論和收藏。也是希望如果本專欄真的對大家有幫助可以點個贊,有建議或者疑惑可以在下方隨時問。
先預告一下【V少JS基礎班】的全部內容,我做了一些調整。看著很少,其實,正兒八經細分下來其實挺多的,第一個月的東西也一點不少。
第一個月【變量 、作用域 、BOM 、DOM 、 數據類型 、操作符】
第二個月【函數、閉包、this、面向對象編程】
第三個月【原型鏈、異步編程、nodejs】
第四個月【密碼學、各類加密函數】
第五個月【jsdom、vm2、express】
第六個月【基本請求庫、前端知識對接】
==========================================================
二、本節涉及知識點
閉包
==========================================================
三、重點內容
一、概念
1- 閉包
概念:
首先我們看下閉包的權威解釋(來自MDN)
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
翻譯過來就是:
閉包是由捆綁起來(封閉的)的函數和函數周圍狀態(詞法環境)的引用組合而成。換言之,閉包讓函數能訪問它的外部作用域。在 JavaScript 中,閉包會隨著函數的創建而同時創建。
翻譯:
英文 | 中文 | 解釋 |
---|---|---|
combination | 組合,結合 | 結合體 |
bundled together / enclosed | 封裝在一起 | 函數和變量一起被打包 |
references | 引用 | 指向外部變量的引用 |
surrounding state | 周圍狀態 | 外層作用域中的變量 |
lexical environment | 詞法環境 | 在函數創建時所在的作用域鏈 |
提煉:
closure是閉包
閉包不是函數, 閉包是包含函數與函數所帶的詞法環境綁定的結合體
口語解釋:
閉包是一個結構體。 他是一個函數和一個函數所攜帶的詞法環境一起構成的結構體。
那我們現在就有一個問題了, 函數我們知道,詞法環境是什么
2-詞法環境&詞法作用域
還是MDN:
A Lexical Environment is a specification type used to define the association of Identifiers (names) with specific variables and functions based on the lexical nesting structure of ECMAScript code.
a specification type: 規格類型
the association of: 關聯
Identifiers: 標識符
specific variables: 特定變量
nesting structure of: 嵌套結構
提煉:
Lexical Environment 是引擎內部用來描述和追蹤當前代碼上下文中的變量、函數綁定信息的機制。
口語:
Lexical Environment 和 Lexical Scope 在代碼調試過程中的可見性上可以默認是同一個東西。
但是 Lexical Environment, 他是引擎運行時創建的一個結構(對象)。而Lexical Scope則是代碼層靜態的結構。
在代碼執行時,均被存放在上下文中。
好。 說了這么多概念。 我們可能還是不理解。 那我們直接上代碼。 我們先從詞法作用域【Lexical Scope】開始
【Lexical Scope】
作用域分為: 全局作用域,函數作用域,塊作用域[ES6新增]
二、實例講【作用域】
作用域其實在第一彈的時候就有講過了。當時沒有涉及到這么深,簡單的說明了一下, 現在我們再次總結回顧一下:
作用域是在程序運行時代碼中的某些特定部分中變量、函數和對象的可訪問性。
作用域就是代碼的執行環境,全局作用域就是全局執行環境,局部作用域就是函數的執行環境,它們都是棧內存
作用域又分為全局作用域和局部作用域。在ES6之前,局部作用域只包含了函數作用域,ES6的到來為我們提供了 ‘塊級作用域’(由一對花括號包裹),可以通過新增命令let和const來實現;而對于全局作用域
在 Web 瀏覽器中,全局作用域被認為是 window 對象,因此所有全局變量和函數都是作為 window 對象的屬性和方法創建的。
.在一個函數內部
2.在一個代碼塊(由一對花括號包裹)內部
let 聲明的語法與 var 的語法一致。基本上可以用 let 來代替 var 進行變量聲明,但會將變量的作用域限制在當前代碼塊中 (注意:塊級作用域并不影響var聲明的變量)
以上是對作用域的一個總結回顧。 具體細節我們再來討論。
作用域分為: 全局作用域,函數作用域,塊作用域[ES6新增]
1- 函數作用域:
指的是在 JavaScript 中,函數內部聲明的變量和函數只能在該函數內部訪問,外部無法直接訪問。
也就是說,每當你聲明一個函數,函數內部就創建了一個獨立的作用域。
function foo() {let a = 10;console.log(a); // 10
}
foo();
console.log(a); // ReferenceError: a is not defined
變量a在函數foo內聲明,函數外無法訪問,會報錯。
額外小知識:
作用域鏈: 當函數內訪問變量時,會先查找自己作用域內有沒有這個變量,如果沒有,就去外層作用域找,直到找到全局作用域。
2- 塊級作用域:
塊級作用域就是被一對花括號 {} 包圍起來的代碼塊,里面用 let 或 const 聲明的變量,只在這對花括號內有效。
這意味著變量的生命周期和可訪問范圍嚴格限制在該代碼塊內。
3- 全局作用域(Global Scope):是 JavaScript 中最頂層的作用域,
任何在函數、塊外部聲明的變量和函數,都屬于全局作用域。
這里就要說一下window對象了。 在ES6之前, window就等同于全局作用域。
但是在ES6之后, JavaScript引入了 let和const的概念。
就導致,window其實只是全局作用域的一部分了。
我們用以下這個圖并配上代碼去理解
當你運行 JS 時,JavaScript 引擎為每段代碼創建一個執行上下文(Execution Context),它包含三個核心組件:
執行上下文(Global / Function)
├── VariableEnvironment(var/function)
├── LexicalEnvironment(let/const/class)
├── ThisBinding(this 綁定)
以下代碼中的b,c在全局作用域中,但并未掛載到window上
var a = 1;
let b = 2;
const c = 3;console.log(window.a); // 1
console.log(window.b); // undefined
console.log(window.c); // undefined
原理圖如下:
┌──────────────┐
│ GlobalExecutionContext │
│ ┌──────────────┐ │
│ │ VariableEnv │ → window (global object)
│ │ a: 1 │ │
│ └──────────────┘ │
│ ┌──────────────┐ │
│ │ LexicalEnv │ ← 你訪問不到
│ │ b: 2 │
│ │ c: 3 │
│ └──────────────┘
└──────────────┘
好的,那我們接下來進入到了關鍵點了。
三、let和const 的引入與作用域鏈
我們先看下以下代碼:
debugger;
var data = [];
for (var i = 0; i < 3; i++) {data[i] = function () {console.log(i);};
};
data[0]();
data[1]();
data[2]()輸出的結果為:
3
3
3
為什么呢?我們可以看下流程。 代碼執行就是順序執行。
先用var聲明了一個 data=[]
然后我們進入循環, 3次循環分別賦值
data[0] = function () {console.log(i);}
data[1] = function () {console.log(i);}
data[2] = function () {console.log(i);}
而此時的i是var聲明的,所以掛載在window上。經過3次循環
window.i 的值就為3。 向下執行的時候。
data[0]();
data[1]();
data[2]()開始調用函數, 將window.i傳入。 返回的值就是3
那有沒有辦法去解決這個問題呢,我就想打印出0,1,2。 肯定是有的, 這就是let的作用。 我們只用將 var i = 0 換成 let i = 0
var data = [];
for (let i = 0; i < 3; i++) {data[i] = function () {console.log(i);};
};
data[0]();
data[1]();
data[2]()此時的輸出結果就是:
0
1
2
為什么呢? 我們再看下流程
先用var聲明了一個 data=[]
然后我們進入循環, 3次循環分別賦值
data[0] = function () {console.log(i);}
data[1] = function () {console.log(i);}
data[2] = function () {console.log(i);}
而此時的i是let聲明的,所以他掛載在塊級作用域上。 三次循環生成3個塊級作用域,分別綁定在三個函數上。
data[0]();
data[1]();
data[2]()
向下執行的時候, 三個函數分別使用綁定的塊級作用域中的i。 返回值就是0, 1 , 2
總結:
這個案例我們了解到了。
第一: 作用域鏈的存在,代碼在函數或者塊級作用域中(局部作用域)執行的時候。會優先獲取當前作用域中的變量,如果找不到,會向上層查找。
第二:let和const在塊級作用域中聲明的變量不會穿掛載到window上。 而var聲明的變量可以掛在到window上。
第三:window 與 全局作用域不全等
四、閉包初始模樣
到此,我們其實已經離閉包越來越近了。 甚至我們已經用到了閉包。
for (let i = 0; i < 3; i++) {data[i] = function () {console.log(i);};
};
以上代碼,就是一個很標準的閉包結構。
我們一直都在背誦兩個概念。
1- 函數內部嵌套另一個函數
2- 內函數返回外函數
滿足這兩個條件就是閉包。
其實這是完全錯誤的說法。 我們再次回顧一下開頭我們從MDN中查找的概念:
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
翻譯:
閉包是由捆綁起來(封閉的)的函數和函數周圍狀態(詞法環境)的引用組合而成。換言之,閉包讓函數能訪問它的外部作用域。在 JavaScript 中,閉包會隨著函數的創建而同時創建。
首先閉包是個結構,他不是一個函數。閉包是有函數和他組成的詞法環境構成。
其次,閉包讓函數能訪問他的外部作用域
我們對照這個案例看一下:
我們有函數:
function () {console.log(i);};
也有作用域:
塊級作用域中的i,被內層函數訪問。
所以 function 函數 和 塊級作用域 共同構建成了一個閉包結構。
五、我們熟悉的閉包
好的, 至此。我們已經完全理解了閉包。 那我們把塊級作用域改寫成函數作用域。
再來看一下我們一直在背誦的邏輯
var result = [];
var a = 3;
function foo(a) {let total = 0; for (var i = 0; i < 3; i++) {result[i] = function () {total += i * a;console.log(total);}}
}
foo(1);
result[0]();
result[1]();
result[2]();
此時,我們把塊級作用域跟換成了函數作用域。 雖然我們使用了var去聲明了var i=0; 但是var一直在函數作用域內。
并且有內部的result[i]指向的函數使用。 此時就是我們最常熟知的閉包結構。
最后在這里, 我再次聲明一下。 閉包是一個結構,他不是一個函數。 閉包是包含了 函數和它所關聯的作用域,一起構成的一個結構
另外我們如果用outer函數來表示外部函數,inner函數表示內部函數。 我們的閉包應該是inner和他的作用域一起構成了一個閉包。
===========================================================
六、閉包的實際應用
ok,閉包的原理我們算是真正的理解了。
我們現在知道了原理,那我們要思考的應該是,我們為什么要用閉包呢?
其實在循環中使用let,已經跟我們說明了。 為什么我們要使用閉包。
當我們需要私有化變量的時候,我們就需要用到閉包。 我們不想var出來的變量直接穿透我們的作用域。
不想再我們的結構外層還有操作可以改動我們的變量時,就需要使用閉包了。
看以下代碼:
const counter = (function () {let privateCounter = 0;function changeBy(val) {privateCounter += val;}return {increment() {changeBy(1);},decrement() {changeBy(-1);},value() {return privateCounter;},};
})();console.log(counter.value()); // 0counter.increment();
counter.increment();
console.log(counter.value()); // 2counter.decrement();
console.log(counter.value()); // 1
此時,在counter對象中。 privateCounter就是counter的私有變量。 在外面是無法改動函數內部privateCounter的值。
這就是閉包的應用
七、慎用閉包
其實對我們爬蟲來說, 我們是不需要使用閉包的,我們要做到的是理解閉包的原理。 從而讓我們更好的懂開發邏輯,懂逆向。
我們學習完閉包之后,可能就會覺得,閉包真是個好東西。我們應該多用閉包,甚至每次要私有化變量的時候都用一個外函數套內函數的方式好了。
但是過多的使用閉包會有一個問題, 每個閉包都有它的私有化變量。他們互相隔離,單獨的占用一塊內存。其實對性能的損耗是很大的。
所以在正常開發中,正確的處理思路是,把需要共享的變量綁定在原型鏈上,我們可以通過操作原型鏈來控制對應的變量。
這樣就能在解決屬性綁定的問題同時解決內存。那原型鏈我們還不清楚,我們下一章節就是原型鏈的講解
最后的最后
本章就到這里。拖得太久,感謝各位佬的收看。如果覺得寫得好,還請麻煩點贊收藏。確實是因為之前寫文章大家都比較積極的點贊收藏給了我很大的動力。感謝各位大佬