前言:JavaScript深入理解scope作用域和閉包,就要先理解什么是執行背景關系和執行堆疊,作用域、作用域鏈、閉包是JavaScript的難點也是重點,其實理解起來也不難,學習過其他語言,比如C語言就可以很好的類比,
一 知識儲備
深入學習JavaScript程式內部的執行機制,就要徹底理解執行背景關系和執行堆疊,
了解一些專業概念
- EC:函式執行環境(或執行背景關系),Execution Context
- ECS:執行環境堆疊,Execution Context Stack
- VO:變數物件,Variable Object
- AO:活動物件,Active Object
- scope chain:作用域鏈
二 執行背景關系和執行堆疊
1 什么是執行背景關系?
執行背景關系是評估和執行javascript代碼環境的抽象概念,每當控制器轉到ECMAScript可執行代碼的時候,它都是在執行背景關系中運行,即執行環境中的變數,函式宣告,引數,作用域鏈,this等資訊,
//組成代碼展示
const ExecutionContextObj = {
VO: window, // 變數物件
ScopeChain: {}, // 作用域鏈
this: window
};
2 執行背景關系三種型別
-
全域執行背景關系—這是默認背景關系或者說基礎的背景關系,任何不在函式內部的代碼都在全域背景關系中,瀏覽器中的全域物件就是window物件,它會執行兩件事:創建一個全域的 window 物件(瀏覽器的情況下),并且設定
this的值等于這個全域物件,一個程式中只會有一個全域執行背景關系, -
函式執行背景關系—每當一個函式被呼叫時, 都會為該函式創建一個新的背景關系,每個函式都有它自己的執行背景關系,不過是在函式被呼叫時創建的,函式背景關系可以有任意多個,每當一個新的執行背景關系被創建,它會按定義的順序執行一系列步驟,
-
Eval函式執行背景關系—執行在
eval函式內部的代碼也會有它屬于自己的執行背景關系,JavaScript開發者不經常使用,
3 什么是執行堆疊?
執行堆疊,也叫呼叫堆疊,被用來存盤代碼運行時創建的所有執行背景關系,
堆疊是一種資料結構,遵循后進先出的原則,
當 JavaScript 引擎第一次遇到腳本時,它會創建一個全域的執行背景關系并且壓入當前執行堆疊,每當引擎遇到一個函式呼叫,它會為該函式創建一個新的執行背景關系并壓入堆疊的頂部,引擎會執行那些執行背景關系位于堆疊頂的函式,當該函式執行結束時,執行背景關系從堆疊中彈出,控制流程到達當前堆疊中的下一個背景關系,
function fn1() {
console.log('fn1被呼叫了 -- 創建了fn1的函式執行背景關系,壓入堆疊');
fn2();
console.log('fn2執行完成,fn2的執行背景關系會從堆疊中彈出');
}
function fn2() {
console.log('fn2被呼叫了 -- 創建了fn2的函式執行背景關系,壓入堆疊');
}
fn1();
console.log('fn1執行完成,fn2的執行背景關系會從堆疊中彈出');
運行結果
//fn1被呼叫了 -- 創建了fn1的函式執行背景關系,壓入堆疊
//fn2被呼叫了 -- 創建了fn2的函式執行背景關系,壓入堆疊
//fn2執行完成,fn2的執行背景關系會從堆疊中彈出
//fn1執行完成,fn2的執行背景關系會從堆疊中彈出

4 JavaScript引擎創建執行背景關系
執行背景關系有兩個階段
- 創建階段
- 執行階段
在代碼執行前是創建階段,發生三件事情
- this系結
- 創建(LexicalEnvironment)詞法環境組件
- 創建(VariableEnvironment)變數環境組件
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
(1)創建階段
- this系結
在全域執行背景關系中,this 的值指向全域物件,(在瀏覽器中,this參考window物件)
在函式執行背景關系中,this的指向取決于函式是如何被呼叫的,
let obj = {
fn: function() {
console.log(this);
}
}
let win = obj.fn;
obj.fn(); //this指向obj
win(); // this指向window
- LexicalEnvironment詞法環境
官方ES6檔案概述:詞法環境是一種規范型別,基于 ECMAScript 代碼的詞法嵌套結構來定義識別符號和具體變數和函式的關聯,一個詞法環境由環境記錄器和一個可能的參考外部詞法環境的空值組成,
attention:識別符號指的是變數/函式的名字,而變數是對實際物件[包含函式型別物件]或原始資料的參考,
詞法環境有兩個組成部分
- 宣告式環境記錄器:存盤變數和函式宣告的實際位置
- 物件環境記錄器:可以訪問其外部詞法環境(作用域)
詞法環境有兩種型別
全域環境:是一個沒有外部環境的詞法環境,其外部環境參考為 null,擁有一個全域物件(window 物件)及其關聯的方法和屬性(例如陣列方法)以及任何用戶自定義的全域變數,this 的值指向這個全域物件,
函式環境:用戶在函式中定義的變數被存盤在環境記錄中,包含了arguments 物件,對外部環境的參考可以是全域環境,也可以是包含內部函式的外部函式環境,
attention:
在全域環境中,環境記錄器是物件環境記錄器
在函式環境中,環境記錄器是宣告式環境記錄器
- VariableEnvironment變數環境
在ES6中,詞法環境組件和變數環境組件之間的一個區別是前者用于存盤函式宣告和變數let和const系結,而后者僅用于存盤變數var系結,
(2)執行階段
在此階段,完成對所有這些變數的分配,最后執行代碼,(在執行階段,如果 JavaScript 引擎不能在原始碼中宣告的實際位置找到 let 變數的值,它會被賦值為 undefined
三 作用域(Scope)
1 什么是作用域?
作用域指程式中定義變數的區域,它可以決定當前執行代碼的變數的可訪問權限,按照我自己的理解來說,作用域就是代碼中某些特定的變數、函式在特定的獨立區域可以被訪問到,
舉個例子
function OutFun() {
var inVariable = "internal variable";
}
OutFun();
console.log(inVariable);
為什么會報錯呢?這就跟變數的作用域有關系了!這里先不解釋,看完整篇筆記就懂了,

2 作用域有什么作用呢?
我們給變數起名字的時候,有時候代碼量太多,一不小心就重名了,就像全世界這么多人,總有很多人重名一個道理,那如何保證識人的唯一性呢,對的!可以通過區域劃分,這些人生活在世界的各個角落,區域是不一樣的,所以我們可以通過A住在B城,另外一個A住在C城,從而把A和A區分開來,作用域也是一個道理,不同作用域下的同名變數不會起沖突的原因就是,重名變數各自在不同的區域被訪問到,起到了隔離的作用,
3 作用域的分類
在ES6之前,JavaScript只有全域作用域和函式作用域,在ES6之后,通過提供關鍵字let/const來體現塊級作用域,
(1)全域作用域
在代碼中的任何地方都可以被訪問到的物件就擁有全域作用域,那什么情況下是擁有全域作用域的呢?
- 最外層函式+在最外層函式的外面定義的最外層變數
var outVariable = "I am outermost variable"; //最外層變數
function outFun() { //最外層函式
var innerVariable = "I am internal variable"; //內層變數
function innerFun() { //內層函式
console.log(innerVariable);
}
innerFun();
}
console.log(outVariable);
outFun();
console.log(innerVariable);
innerFun();

為什么控制臺會有這樣的列印資訊呢?
在代碼中我們在最外層函式outFun()里面嵌套了一層內層函式innerFun(),變數outVariable在全域作用域有宣告,所以沒有報錯,變數innerVariable在函式內部被宣告,而在全域作用域沒有宣告,所以在全域作用域下取值會報錯,outFun()函式是外層函式,在全域作用域下被宣告被呼叫,不會報錯,innerFun()函式的宣告嵌套在outFun()函式里面,屬于內層函式,在全域作用域呼叫會報錯,要深入理解這些,建議結合計算機組成原理,了解全域變數和區域變數在計算機內部存盤的分配和使用,
- 未定義直接賦值的變數自動宣告且有全域作用域
function outFun() {
variable = "我是沒有被定義直接賦值的變數";
var invariable = "我是內部變數";
}
outFun();
console.log(variable);
console.log(invariable);

- 一般情況下,window物件擁有全域作用域
window.name;
window.location;
window.top;
弊端
頻繁使用全域變數在全域作用域起作用,會污染記憶體空間,引起變數重名的沖突,造成資源浪費,全域變數記憶體的分配一直要等到程式執行結束才會被釋放,如果一個變數只需要在代碼第一行使用之后就不會再次被使用,但是它命名到全域作用域,那么這一個全域變數就會污染記憶體空間,一直霸占記憶體位置,但實際上沒有任何的使用價值了,(占著茅坑不拉屎)
解決方案—函式作用域
(2)函式作用域
函式作用域是指宣告在函式內部的變數,和全域作用域相反,是區域作用域,在固定的函式代碼段里面才可以被訪問和使用,函式執行完之后記憶體就會釋放為執行這個函式開辟的記憶體空間,就解決了全域變數會引起污染的問題,
function Fun() {
var name = "gaby";
function SayHi() {
alert(name);
}
SayHi();
}
alert(name);
SayHi();
SayHi()函式是內層函式,嵌套在Fun()外層函式里面,所以會造成腳本錯誤,函式作用域顧名思義就是在函式{}括起來的內部可以被訪問有作用,

函式的作用域是分層級的,內層作用域的變數可以訪問外部作用域的變數,反之則不行,
每一個變數都有生命周期
來看代碼例子,加深理解,
function add1(a) {
var b = a + a;
function add2(c) {
console.log(a, b, c);
}
add2(b * 3);
}
add1(2);
根據下圖控制臺的資訊顯示,add2()這個最內層的函式可以依次訪問到變數a,變數b,變數c,

給上面的代碼再增加一行,我們再看一下控制臺的資訊,
function add1(a) {
var b = a + a;
function add2(c) {
console.log(a, b, c);
}
add2(b * 3);
console.log(a, b, c);
}
add1(2);
根據下圖控制臺資訊顯示,控制臺報錯了,原因就是add1()這個外層函式不能訪問add2()這個內層函式的變數,


由上面的例子可以看到,函式變數的訪問順序是按層級劃分的,一層一層查找,
需要注意的是,并不是所有用{}大括號括起來的代碼都是函式作用域,比如回圈if和switch,不會像函式,創建一個新的作用域,
(3)塊級作用域
ES6的新特性,通過let和const關鍵字宣告,所宣告的變數在指定的塊級作用域外無法被訪問,
在什么環境下會被創建呢?
- 在一個函式內部
- 在一個代碼塊{}內部
使用let和const的時候需要注意幾個點
(1)宣告的變數不會提升到代碼塊的頂部,需要手動將宣告放置到頂部,方便變數在整個代碼塊內都可以使用
function getName(condition) {
if (condition) {
let name = "gaby";
return name;
} else {//name在此處不可用
return null;
}
//name在此處不可用
}
(2)不能重復宣告同一個變數
var name = "gaby";
let name = "gaby";

如果一個識別符號在代碼塊內部已經被宣告,那么在這代碼塊內使用let再次宣告會拋出錯誤,name被宣告了兩次,一次是var另一次是let,let不能在同一快作用域內重復宣告一個已經被宣告過的變數,但是如果處理成嵌套的情況,則是可以的,
var name = "gaby";
function fun() {
let name = "gabrielle";
}
(3)實作for回圈的塊級作用域
計數器變數問題(稍微提一下,這個機理說清楚會需要很大的篇幅)
for (let i = 0; i < 10; i++) {
//...
}
console.log(i);
使用let宣告i,計數器變數i只在回圈體內能被訪問到,回圈體外找不到,

我們再來比較var和let宣告的區別
- 用var宣告
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
}
}
a[4]();

為什么輸出是10?
for回圈里面的i用var宣告,在全域作用域內都可以被訪問,全域變數只有一個i,每執行一次回圈體,i的值都會被改變,回圈體內的陳述句console.log(i)里面的i指向的是全域變數,所有a陣列里面的成員都是指向同一個i,執行完之后都為10,
- 用let宣告
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
}
}
a[4]();

為什么輸出是4?
let和var的作用域不同,let在回圈體的每一輪回圈中有效,每一輪回圈i的值都是不一樣,因為每一輪回圈的i都需要被重新宣告,那每一輪都是新的值,是重復的值還是在上一輪回圈的值基礎上進行計算,**是上一輪回圈的值基礎上進行計算!**這是因為JavaScript引擎會記住上一輪回圈的值,
(4)創造父作用域和子作用域
for (let i = 0; i < 4; i++) {
let i = 'gaby';
console.log(i);
}
輸出四次gaby,回圈變數i和函式內部i不是同一個作用域,由各自的單獨的作用域,

四 作用域鏈
1 自由變數
當前作用域沒有定義的變數叫做自由變數,這個自由變數會由內而外向父級一層一層尋找,
var a = 1;
function fun() {
var b = 2;
console.log(a);
console.log(b);
}
fun();

2 作用域鏈
在自由變數向上一層一層尋找,直到找到全域作用域沒有找到就結束,自由變數一層一層查找的關系,就叫做作用域鏈,
var a = 1;
function fun1() {
var b = 2;
function fun2() {
var c = 3;
console.log(a);
console.log(b);
console.log(c);
}
fun2();
}
fun1();

五、閉包
在前面分別整理了要深入理解閉包需要的前綴知識,js原型、原型鏈、繼承+scope作用域、作用域鏈+執行背景關系和執行堆疊+變數的作用域和變數提升等等,在這些前綴知識的基礎上去理解js閉包是不難的,作為一個前端打工人,扎扎實實的js基礎是必要條件,地基不穩樓層又能高到哪里去呢?
1 什么是閉包?
在上一篇筆記中整理了js的scope作用域,不懂回去翻看筆記,能夠訪問其他內部函式變數的函式,成為閉包,
在函式內部定義的函式,被回傳出去并在外部進行呼叫,就是閉包,
function fun1() {
var a = 1;
function fun2() {
console.log(a);
}
return fun2;
}
var fn = fun1();
fn();//形成閉包,從外部獲取內部作用域的資訊,

我們再一次回顧一下,前面整理的知識,代碼究竟是怎么樣的一個運行機制?
(1)編譯階段,變數和函式被宣告,作用域就確定下來,
(2)運行函式fun1(),創建fun1()函式的執行背景關系,內部存盤fun1()中所有的變數函式的資訊,
(3)函式fun1()執行完之后,把fun2的參考賦值給外部變數fn,要明確的是此時fn的指標指向的是fun2,此時fn位于全域作用域,fun2位于函式作用域,所以可以看見fn位于fun1() 作用域之外,但是訪問到了fun1()的內部變數,
(4)fn在全域被執行,內部代碼console.log(a)向作用域請求獲取a變數,在本級的作用域沒有找到,就向上父級作用域一層一層找父親(找爸爸),在fun1()找到了a變數,回傳給console.log所以列印出來了1,
2 閉包被創造出來有什么用?
還是一樣的邏輯,一樣方法被創造出來肯定是用來解決問題,那么閉包可以用來干嘛呢?
(1)屬性私有化
接觸過java語言的都學過java可以通過關鍵字public private 設定訪問權限,但是JavaScript中沒有,物件中的方法和屬性均可以訪問到,沒有隱私空間,可以隨意修改屬性和方法,造成安全隱患,想象一下,你的房間沒有鑰匙,任何人都可以進入訪問你的房間,隨意更改你房間的布置,是不是很可怕的事情,閉包的出現就是來解決這個問題的,模擬屬性和方法的私有化,
function getImformation() {
var name = "gaby";
var age = 20;
return function () {
return {
getName: function () {
return name;
},
getAge: function () {
return age;
}
};
};
}
var obj = getImformation()();
obj.getName();
obj.getAge();
obj.age;

(2)避免重復實體化
單一實體化,保證一個類只有一個實體,避免污染記憶體空間,先判斷實體是否存在,存在就回傳,不存在創建后回傳,
function Gaby() {
this.data = "gaby";
}
Gaby.getInstance = (function () {
var instance;
return function () {
if (instance) {
return instance;
} else {
instance = new Gaby();
return instance;
}
}
})();
var a = Gaby.getInstance();
var b = Gaby.getInstance();
console.log(a === b);
console.log(a.data);

3 閉包會引起什么問題?
JavaScript內部有垃圾回識訓制,用計數的方法 ,當記憶體中的一個變數被參考一次,計數+1,垃圾回識訓制會在固定的時間間隔內詢問這些變數,將計數為0的變數標記為失效變數從而清除釋放記憶體,
再來看第一個閉包代碼,fun1()函式隔絕了外部的影響,所有變數在函式內部完成,fun1()執行后,理論上內部的變數就會被銷毀,記憶體被回收,但是我們寫了一個閉包,這就導致了全域作用域始終存在一個a變數,一直占用記憶體,造成記憶體泄漏,
function fun1() {
var a = 1;
function fun2() {
console.log(a);
}
return fun2;
}
var fn = fun1();
fn();//形成閉包,從外部獲取內部作用域的資訊,
由于閉包使用過度而導致記憶體無法釋放的情況,就叫做記憶體泄漏,
正經一點:記憶體泄露 是指當一塊記憶體不再被應用程式使用的時候,由于某種原因,這塊記憶體沒有返還給作業系統或者記憶體池的現象,記憶體泄漏可能會導致應用程式卡頓或者崩潰,
六、后話
最近也是大三上學期了,也開始到了找實習焦慮的日常,技術崗不缺人才,向上看的人很多,但是愿意向下扎根的人很少,希望每一個coder都能向下扎根也能向上生長,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/339178.html
標籤:其他
