00、頭痛的JS閉包、詞法作用域?
被JavaScript的閉包、上下文、嵌套函數、this搞得很頭痛,這語言設計的,感覺比較混亂,先勉強理解總結一下😂😂😂。
- 爲什麼有閉包這麼個東西?閉包包的是什麼?
- 什麼是詞法作用域?
- 函數是如執行的呢?
01、執行上下文 (execution context)
名稱 | 描述 |
---|---|
是什麼? | 執行上下文 (execution context) 是JavaScript 代碼被解析和執行時所在環境的抽象概念。每一個函數執行的時候都會創建自己的執行上下文,保存了函數運行時需要的信息,並通過自己的上下文來運行函數。 |
幹什麼用的? | 當然就是運行函數自身的,實現自我價值。 |
有那些種類? | ① 全局上下文:全局環境最基礎的執行上下文,所有不在任何函數內的代碼都在這裏面。 🔸 瀏覽器中的全局對象就是 window ,全局作用域下var 申明、隱式申明的變量都會成爲全局屬性變量,全局的this 指向window 。🔸 其中會初始化一些全局對象或全局函數,如代碼中的 console 、undefined 、isNaN ② 函數上下文:每個函數都有自己的上下文,調用函數的時候創建。可以把全局上下文看成是一個頂級根函數上下文。 ③ eval() 調用內部上下文:eval 的代碼會被編譯創建自己的執行上下文,不常用也不建議使用。基於這個特點,有些框架會用eval() 來實現沙箱Sandbox。 |
保存了什麼信息? | 初始化上下文的變量、函數等信息 🔸 thisValue: this 環境對象引用。🔸 內部(Local)環境:函數本地的所有變量、函數、參數(arguments)。 🔸 作用域鏈:具有訪問作用域的其他上下文信息。 |
誰來用? | 執行上下文由函數調用棧來統一保存和調度管理。 |
生命週期 | 創建(入棧)=> 執行=> 銷燬(出棧),函數調用的時候創建,執行完成後銷燬。 |
02、函數調用棧是幹啥的?
函數調用棧(Function Call Stack),管理函數調用的一種棧式結構(後進先出 )隊列,或稱執行棧,存儲了當前程序所有執行上下文(正在執行的函數)。最早入棧的理所當然就是程序初始化時創建的全局上下文
了,他是VIP會員,會一直在棧底,直到程序退出。
2.1、函數執行流程
🟢函數執行上下文調用流程(也是函數的生命週期):
- 創建-入棧:創建執行上下文,並壓入棧,獲得控制權。
- 執行-幹活:執行函數的代碼,給變量賦值、查找變量。如有內部函數調用,遞歸重複函數流程。
- 出棧-銷燬:函數執行完成出棧,釋放該執行上下文,其變量也就釋放了,全都銷燬,控制權回到上一層執行上下文。
function first() {
second(); //調用second()
}
function second() {
}
first();
上面的代碼執行過程如下圖所示:
- 程序初始化運行時,首先創建的是全局上下文
Global
,進入執行棧。 - 調用
first()
函數,創建其下文併入棧。 first()
函數內部調用了second()
函數,創建second()
下文入棧並執行。second()
函數執行完成並出棧,控制權回到first()
函數上下文。first()
函數執行完成並出棧,控制權回到全局上下文。
🌰再來一個函數調用棧的示例:
var a = 1;
let b = 1;
function FA(x) {
function FB(y) {
function FC(z) {
console.log(a + b + x + y + z);
}
FC(3);
}
FB(2);
}
FA(1); //8
上面函數在執行FC()
時的函數調用堆棧如下圖(Edge瀏覽器斷點調試):
✅ 執行FC
函數代碼時,其作用域保留了所有要用到的作用域變量,從自己往上,直到全局對象,閉包就是這麼來的!
var a = 1;
:var申明的變量會作爲全局對象window
的變量。let b = 1;
:全局環境申明的變量,任何函數都可以訪問,放在全局腳本環境中,可以看做全局的一部分。
✅ 調用堆棧中有FC、FB、FA,因爲是嵌套函數,FB、FA並未結束,所以還在堆棧中,函數執行完畢就會被立即釋放拋棄。
2.2、堆棧溢出
📢 函數調用棧容量是有限的!—— 遞歸函數
遞歸函數就是一個多層+自我嵌套調用的過程,所以執行遞歸函數時,會不停的入棧,而沒有出棧,循環次數太多會超出堆棧容量限制,從而引發報錯。比如下面示例中一個簡單的加法遞歸,在Firefox瀏覽器中遞歸1500次,就報錯了(InternalError: too much recursion),Edge瀏覽器是11000次超出調用棧容量(Maximum call stack size exceeded)。
❓怎麼解決呢?
- 避免遞歸:封裝處理邏輯,轉換成循環的方式來處理。或用
setTimeout(func,0)
發送到任務隊列單獨執行。 - 拆分執行:合理拆分代碼爲多個遞歸函數。
function add(x) {
if (x <= 0)
return 0;
return x + add(x - 1); //遞歸求和
}
add(1000); //Firefox:1000可以,1500就報錯 InternalError: too much recursion
add(10000);//Edge:10000可以執行,11000就報錯 Maximum call stack size exceeded
» Firefox 的調用堆棧:
03、什麼是詞法作用域?
作用域(scope)就是一套規定變量作用範圍(權限),並按此去查找變量的規則。包括靜態作用域、動態作用域,JavaScript中主要是靜態作用域(詞法作用域)。
- 🔴 靜態作用域(就是詞法作用域):JavaScript是基於詞法作用域來創建作用域的,基於代碼的詞法分析確定變量的作用域、作用域關係(作用域鏈)。詞法環境就是我們寫代碼的順序,所以是靜態的,就是說函數、變量的作用域是在其申明的時候就已經確定了,在運行階段不再改變。
- 🟡 動態作用域:基於動態調用的關係確定的,其作用域鏈是基於運行時的調用棧的。比如
this
,一般就是基於調用來確定上下文環境的。因此this
值可以在調用棧上來找,注意的是this
指向一個引用對象,不是函數本身,也不是其詞法作用域。
因此,詞法作用域主要作用是規定了變量的訪問權限,確定瞭如何去查找變量,基本規則:
- 代碼位置決定:變量(包括函數)申明的的地方決定了作用域,跟在哪調用無關。
- 擁有父級權限:函數(或塊)可以訪問其外部的數據,如果嵌套多層,則遞歸擁有父級的作用域權限,直到全局環境。
- 函數作用域:只有函數可以限定作用域,不能被上級、外部其他函數訪問。
- 同名就近使用:如果有和上級同名的變量,則就近使用,先找到誰就用誰。
- 逐層向上查找:變量的查找規則就是先內部,然逐級往上,直到全局環境,如果都沒找到,則變量
undefined
。
這裏的詞法作用域,就是前文所說JS變量作用域。而閉包保留了上下文作用域的變量,就是爲了實現詞法作用域。
❓那詞法作用域是怎麼實現的呢?——作用域鏈、閉包
父級函數FA()
執行完成後就出棧銷燬了(典型場景就是返回函數)FB()
可以到任何地方執行,那內部函數FB()
執行的時候到哪裏去找父級函數的變量x
呢?
- ✅ 函數內部作用域:首先每個函數執行都會創建自己作用域(執行上下文),查找變量時優先本地作用域查找。
- ✅ 閉包:引用的外部(詞法上級)函數作用域就形成了一個閉包,用一個
Closure
_(Closure /ˈkləʊʒə(r)/ 閉包)_對象保存,多個(外部引用)逐級保存到函數上下文的[[Scope]]
(Scope /skoʊp/ 作用域)集合上,形成作用域鏈。 - ✅ 作用域鏈的最底層就是指向全局對象的引用,她始終都在,不管你要不要她。
- ✅ 變量查找就在這個作用域鏈上進行:自己上下文(詞法環境,變量環境) => 作用域鏈逐級查找=> 全局作用域 =>
undefined
function FA(x) {
function FB(y) {
x+=y;
console.log(x);
}
console.dir(FB);
return FB; //返回FB()函數
}
let fb = FA(1); //FA函數執行完成,出棧銷燬了
fb(2); //3 //返回的fb()函數保留了他的父級FA()作用域變量x
fb(2); //5 //閉包中的x:我又變大了
fb(2); //7 //同一個閉包函數重複調用,內部變量被改變
📢閉包簡單理解就是,當前環境中存放在指向父級作用域的引用。如果嵌套子函數完全沒有引用父級任何變量,就不會產生閉包。不過全局對象是始終存在其作用域鏈[[Scope]]上的。
🌰舉個例子:
var a = 1;
let b = 2;
function FunA(x) {
let x1 = 1;
var x2 = 2;
function FunB(y) {
console.log(a + b + x + x1 + x2 + y);
}
FunB(2);
console.dir(FunB)
}
FunA(1); //9
console.dir(FunA)
上面的代碼示例中,FunA()
函數嵌套了FunB()
函數,如下圖FunB()
函數的[[Scope]]
集合上有三個對象:
Closure (FunA)
FunA()
函數的閉包,包含他的參數x
、私有變量x1
、x2
。Script
:Script Scope 腳本作用域(可以當做全局作用域的一部分),存放全局Script腳本環境內可訪問的let
、const
變量,就是全局作用域內的變量。var
變量a
被提升爲了全局對象window
的“屬性”了。Global
:全局作用域對象,就是window,包含了var
申明的變量,以及未申明的變量。
如果把
FunB()
函數放到外面申明,只在FunA()
調用,其作用域鏈就不一樣了。
04、執行上下文是怎麼創建的?
執行上下文的創建過程中會創建對應的詞法作用域,包括詞法環境和變量環境。
- 創建詞法環境(LexicalEnvironment):
- 環境記錄
EnvironmentRecord
:記錄變量、函數的申明等信息,只存儲函數聲明和let/const
聲明的變量。 - 外層引用
outer
:對(上級)其他作用域詞法環境的引用,至少會包含全局上下文。
- 環境記錄
- 創建變量環境(VariableEnvironment):本質上也是詞法環境,只不過他只存儲
var
申明的變量,其他都和詞法環境差不多。
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
❗變量查找:變量查找的時候,是先從詞法環境中找,然後再到變量環境。就是優先查找const、let變量,其次才var變量。
換幾個角度來總結下,創建執行上下文主要搞定下面三個方面:
① 確定 this 的值(This Binding):
- 在全局上下文中,
this
指向window
。- 函數執行上下文中,如果它被一個對象引用調用,那麼
this
的值被設置爲該對象,否則this
的值被設置爲全局對象或undefined
(嚴格模式下)- call(thisArg)、apply(thisArg)、bind(thisArg)會直接指定
thisValue
值。
② 內部環境:包括詞法環境和變量環境,就是函數內部的變量、函數等信息,還有參數arguments
信息。
③ 作用域鏈(外部引用):外部的詞法作用域存放到函數的[[Scope]]
集合裏,用來查找上級作用域變量。
05、❓有什麼結論?
- ❓ 變量越近越好:最好都本地化,儘量避免讓變量查找鏈路過長,一層層切換作用域去找也是很累的。
- ❓ 優先
const
,其次let
,儘量(堅決)不用var
。 - ❓ 注意函數調用堆棧的長度,比如遞歸。
- ❓ 閉包函數使用完後,手動釋放一下,
fun = null;
,儘早被垃圾回收。 - ❓儘量避免成爲全局環境的變量,特別是一些臨時變量,全局對象始終都在,不會被垃圾回收。
- 包括全局環境申明的的
let
、const
、var
- 切記不用未申明變量
str=''
,不管在哪裏都會成爲全局變量。
- 包括全局環境申明的的
遠離JavaScript、遠離前端......我以爲已經學會了,其實可能還沒入門。
10、GC內存管理
值類型變量的生命週期隨函數,函數執行完就釋放了。垃圾回收GC(Garbage Collection)內存管理主要針對引用對象,當檢測到對象不再會被使用,就釋放其內存。GC是自動運行的,不需干預也無法干預。
GC回收一個對象的關鍵就是——確定他確是一個廢物,麼有任何地方使用他了,主要採用的方法就是標記清理。
- 標記清理(mark-and-sweep):標記內存中的所有的可達對象(根和他所有引用的對象),剩下的就是沒人要的,可以刪除了。
引用計數:按變量被引用的次數,這個策略已不再使用了,由於該回收垃圾的策略太垃圾從而被拋棄了。
❓什麼是可達性?
- 🔸根(roots):當前執行環境(window)最直接的變量,包括當前執行函數的局部變量、參數;當前函數調用鏈上的其他函數的變量、參數;全局變量。
- 🔸可達性(Reachability):如果一個值(對象)可以從根開始鏈式訪問到他,就是可達的,就說明這個數據對象還有利用價值。
上圖中FuncA
函數中的局部變量 obj1
,其值對象{P}
存放在內存堆中,此時的值對象{P}
被根變量obj1
引用了,是可達的。
- 如果函數執行完畢,函數就銷燬了,變量引用
obj1
也一起隨她而去。值對象{P}
就沒有被引用了,就不可達了。 - 如果在函數中顯示執行
obj1=null;
同樣的值對象{P}
沒有被引用了,就不可達了。
GC定期執行垃圾回收的兩個步驟:
① 標記階段:找到可達對象並標記,實際的算法會更加精細。
- 垃圾收集器找到所有的根,並“標記”(記住)它們。
- 繼續遍歷並“標記”被根引用的對象。
- ...繼續遍歷,直到找到所有可達對象並標記。
② 清除階段:沒有被標記的對象都會被清理刪除。
⚠️全局變量不會被清理:屬於window的全局變量就是根,始終不會被清理,有背景靠山就是不一樣!
©️版權申明:版權所有@安木夕,本文內容僅供學習,歡迎指正、交流,轉載請註明出處!原文編輯地址-語雀