5.1 理解閉包
閉包允許函式訪問並操作函式外部的變數,只要變數或函式存在於宣告函式時的作用域內,閉包就可以訪問這些變數和函式
//全域性閉包 不明顯
var outerValue = "ninja";
function outerFunction(){
outerValue === "ninja"; //true
}
outerFunction();
複製程式碼
//閉包例子
var outerValue = "samurai";
var later;
function outerFunction(){
var innerValue = "ninja";
function innerFunction(){
outerValue === 'samurai'; //true
innerValue === 'ninja'; //true
}
later = innerFunction();
}
outerFunction();
later();
複製程式碼
當在外部函式中宣告內部函式時,不僅定義了函式的宣告,還建立了一個閉包
該閉包不僅包含了函式的宣告,還包含了 函式宣告時該作用域中的所有變數
閉包建立了被定義時的作用域內的變數和函式的安全氣泡
每一個通過閉包訪問變數的函式 都具有一個作用域鏈,作用域鏈包含閉包的全部資訊。
:star: 閉包所有的資訊都儲存在記憶體中,直到 JavaScript 引擎確保這些資訊不再使用或頁面解除安裝時,才會清理。
5.2 使用閉包
封裝私有變數
私有變數 是對外部隱藏的物件屬性
function Ninja(){
var feints = 0;
this.getFeints = function(){
return feints;
}
this.feint = function(){
feints++;
}
}
var ninja1 = new Ninja();
ninja1.feint();
ninja1.feints //無法直接從外部訪問屬性
ninja1.getFeints() == 1 //true 可以通過方法來訪問
var ninja2 = new Ninja(); //建立一個新的物件例項,新物件例項作為上下文 this指向新的物件例項
ninja2.getFeints() == 0 //true 新例項有自己的私有變數
複製程式碼
:zap: 在構造器中隱藏變數,使其在外部作用域中不可訪問,但是可在閉包內部進行訪問
- 通過變數 ninja , 物件例項是可見的
- 因為 feint 方法在閉包內部,因此可以訪問變數 feints
- 在閉包外部,無法訪問變數 feints
回撥函式
//在 interval 的回撥函式中使用閉包
//回撥函式中 this 指向變了
function animateIt(elementId){
var elem = document.getElementById(elementId);
var tick = 0;
var timer = setInterval(function(){
if(tick < 100){
elem.style.left = elem.style.top = tick + 'px';
tick++;
}else{
clearInterval(timer);
tick === 100; //true
}
},10);
}
animateIt("box1");
//通過在函式內部定義變數,並基於閉包,使得在計時器的回撥函式中可以訪問這些變數,每個動畫都能夠獲得屬於自己的"氣泡"中的私有變數
複製程式碼
閉包內的函式 不僅可以在閉包建立時可以訪問這些變數,而且可以在閉包函式執行時,更改這些變數的值。
閉包不是在建立的那一時刻的快照,而是一個真實的狀態封裝,只要閉包存在,就可以對變數進行修改。
5.3 執行上下文跟蹤程式碼
JavaScript 有兩種型別的程式碼:一種是全域性程式碼,一種是函式程式碼
上下文分為兩種:全域性執行上下文和函式執行上下文
全域性執行上下文只有一個,當 JavaScript 程式開始執行時就已經建立了全域性上下文;而函式執行上下文在每次 呼叫 函式時,就會建立一個新的。
JavaScript 是單執行緒的:一旦發生函式呼叫,當前的執行上下文必須停止執行,並建立新的函式執行上下文來執行函式。當函式執行完成後,將函式執行上下文銷燬,並重新回到發生呼叫時的執行上下文中。
棧 ?的活塞
//建立執行上下文
function skulk(ninja){
report(ninja + " skulking");
}
function report(message){
console.log(message);
}
skulk("Kuma");
skulk("Yoshi");
複製程式碼
可以通過 JavaScript 偵錯程式中檢視,在 JavaScript 偵錯程式中可以看到對應的呼叫棧(call stack)。
5.4 使用詞法環境跟蹤變數的作用域
詞法環境 是 JavaScript 引擎內部用來跟蹤識別符號與特定變數之間的對映關係
詞法環境 是 JavaScript 作用域的內部實現機制,稱為 作用域 (scopes)
程式碼巢狀
詞法環境主要基於程式碼巢狀,通過程式碼巢狀可以實現程式碼結構包含另一程式碼結構
在作用域範圍內,每次執行程式碼時,程式碼結構都獲得與之關聯的詞法環境。
內部程式碼結構可以訪問外部程式碼結構中定義的變數。
程式碼巢狀與詞法環境
每個執行上下文都有一個與之關聯的詞法環境,詞法環境中包含了在上下文中定義的識別符號對映表。=> 作用域鏈
在特定的執行上下文中,我們的程式不僅直接訪問詞法環境中定義的區域性變數,而且還會訪問外部環境中定義的變數。
無論何時建立函式,都會建立一個與之相關聯的詞法環境,並儲存在名為 [[Environment]]
的內部屬性上。兩個中括號表示是內部屬性。
函式都有詞法環境
var ninja = "Muneyoshi";
function skulk(){
var action = "Skulking";
function report(){
var intro = "Aha!";
assert(intro === "Aha!","Local");
assert(action === "Skulking","Outer");
assert(ninja === "Muneyoshi","Global");
}
report();
}
skulk();
複製程式碼
:question: 為什麼不直接跟蹤整個執行上下文搜尋識別符號,而是通過詞法環境來跟蹤呢?
JavaScript 函式可以作為任意物件進行傳遞,定義函式時的環境與呼叫函式的環境往往是不同的(閉包)
:zap: 無論何時呼叫函式,都會建立一個新的執行環境,被推入執行上下文棧。此外,還會建立一個與之關聯的詞法環境。外部環境與新建的詞法環境,JavaScript 引擎將呼叫函式的內建[[Environment]]屬性與建立時的環境進行關聯。
5.5 理解 JavaScript 的變數型別
3個關鍵字定義變數:var let const
不同之處:可變性、詞法環境
vs 不可變
const 不可變,let var 可變
宣告時需要初始化,一旦宣告完成之後,其值不可更改。 => 指向不可更改
const firstConst = "samurai";
firstConst = "ninja"; //報錯
const secondConst = {};
secondConst.weapon = "wakizashi"; //不報錯
const thirdConst = [];
thirdConst.push("Yoshi"); //不報錯
複製程式碼
如果 const 的值是 靜態變數,則不允許重新賦值;如果 const 的值是物件或者是陣列型別,則可以對其增加新元素,但是不能重寫。其實不可變的是引用,而不是值。
vs 詞法環境
var 一組,let 和 const 一組
關鍵字 var :變數是在距離最近的 函式內部 或是在 全域性詞法環境 中定義的。忽略塊級作用域
var 宣告的變數實際上總是在 距離最近的函式內 或 全域性詞法環境中 註冊的,不關注塊級作用域
let 與 const 直接在最近的詞法環境中定義變數(可以是塊級作用域內、迴圈內、函式內或全域性環境內)。我們可以用 let 和 const 定義 塊級別、函式級別、全域性級別的變數。
詞法環境註冊識別符號
詞法作用域又叫靜態作用域,因為js的作用域在詞法解析階段就確定了
動態作用域:區別於靜態作用域,即在函式呼叫時才決定作用域
JavaScript 程式碼的執行 分兩個階段:
一旦建立了新的詞法環境,就會執行第一階段。在第一階段,沒有執行程式碼,而是JavaScript引擎會訪問並註冊在當前詞法環境中所宣告的變數和函式。變數和函式宣告提升
第二階段的執行取決於變數的型別(let var const 函式宣告)以及環境型別(全域性環境、函式環境或塊級作用域)
1.如果是建立一個函式環境,那麼建立形參及函式引數的預設值。如果是非函式環境,將跳過此步驟。
2.如果是建立全域性或函式環境,就掃描當前程式碼進行函式宣告(不會掃描其他函式的函式體),但是不會掃描函式表示式或箭頭函式。對於所找到的函式宣告,將建立函式,並繫結到當前環境與函式名相同的識別符號上。若該識別符號已經存在,那麼該識別符號的值將被重寫。如果是塊級作用域,將跳過此步驟。
3.掃描當前程式碼進行變數宣告。在函式或全域性環境中,找到所有當前函式以及其他函式之外通過 var 宣告的變數,並找到所有在其他函式或程式碼塊之外通過 let 或 const 定義的變數。在塊級環境中,僅查詢當前塊中通過 let 或 const 定義的變數。對於所查詢到的變數,若該識別符號不存在,進行註冊並將其初始化為 undefined。若該識別符號已經存在,將保留其值。
若函式是函式宣告進行定義的,則可以在函式宣告之前訪問函式。
若函式是函式表示式或箭頭函式進行定義的,則不會在之前訪問到函式
變數的宣告會提升至函式頂部,函式的宣告會提升至全域性程式碼頂部。
其實,變數和函式的宣告並沒有實際發生移動,只是在程式碼執行之前,先在詞法環境中進行註冊。
5.6 閉包的工作原理
閉包可以訪問建立函式時所在作用域內的全部變數
//通過函式訪問私有變數,而不通過物件訪問
function Ninja(){
var feints = 0;
this.getFeints = function(){
return feints;
}
this.feint = function(){
feints++;
};
}
var ninja1 = new Ninja();
ninja1.feint();
var imposter = {};
imposter.getFeints = ninja1.getFeints;
imposter.getFeints() == 1 //true
複製程式碼
JavaScript 中沒有真正的私有物件屬性,但是可以通過閉包實現一種可接受的“私有”變數的方案