閉包是 JavaScript 程式語言的特色。
JavaScript 引擎會確保在執行任何函式時,可以取用到應該要取用到的變數。
雖然 JS 沒有一個閉包物件可以讓你檢視資訊,因此不容易看出這樣的結構,但這些資訊都需要保存在記憶體中,直到 JS 引擎認為不需要他,或直到頁面卸載。
因此可以使用閉包的特性來實現私有變數
function Ninja() {
var feints = 0;
this.getFeints = function(){
return feints;
};
this.feint = function(){
feints++;
};
}
var ninja1 = new Ninja();
ninja1.feint();
ninja1.feints === undefined; // 無法存取 feints 變數
ninja1.getFeints() === 1; // 可以更改私有變數
var ninja2 = new Ninja();
ninja2.getFeints() === 0; // 擁有自己的 feints 變數
5.3 使用執行背景空間 ( execution context ) 追蹤程式執行
無論何時執行JS、它都會在執行背景空間裡執行。
當程式被 JavaScript 執行時,每個敘述句都在一個特定的執行背景空間中執行。
每個執行背景空間,都會對應一個字彙環境。
執行背景空間在物理意義上包含:
* 正在執行的程式碼
* 撰寫的 code
* 看不見的 code -> 你寫的code其實正在被轉換、被另一個別人所寫的程式(編譯器compiler)處理
執行背景空間只有兩種,分別是:
1. 全域執行背景空間 -> 只會有一個
2. 函式執行背景空間 -> 每次呼叫函式都會建立一個新的執行背景空間
5.4 使用字彙環境 ( lexical environment ) 來追蹤識別項
字彙環境是 JavaScript 引擎的一個內部結構,用於追蹤從識別項到特定變數的對應 ( mapping )。
口語上將 lexical environment 稱作 scope。
字彙環境就是代表程式碼在程式中的位置。
[[Environment]]
是函式的內部屬性,不能被外部存取或操作。
當建立函式時,指向該函式所屬字彙環境的參照,會儲存在該函式的 [[Environment]]
的內部屬性中
var a = 1;
var b = 2;
function foo() { // foo 的外部環境為 global 的 lexical environment,而外部環境的 reference 儲存在 [[Evnironment]]
var b = 3
function bar() { // bar 的外部環境為 foo 的 lexical environment,而外部環境的 reference 儲存在 [[Evnironment]]
var c = 4;
function tmp() {
var test = 1;
}
tmp();
}
bar();
}
foo();
當呼叫函式時,會建立一個函式執行背景執行空間與字彙環境,並將該函式的 [[Environment]]
作為字彙環境的外部環境參照
function foo() {
function bar() {
}
bar();
// 呼叫 bar 時,會建立 bar 的 函式執行背景執行空間 與 字彙環境,且外部參照為 bar.[[Environment]],因此外部參照就是 foo 字彙環境
}
每個 function 有一個 [[Scopes]]
屬性,而裡面的資訊就是參照的環境,而參照的環境就是從 [[Environment]]
裡面儲存的 reference 決定的。
5.5 暸解 JavaScript 的變數類型
建立字彙環境,分為兩個步驟
1. 註冊階段
2. 執行階段
全域環境的註冊過程
console.log(globalFun()); // globalFun
console.log(globalVar); // undefined
// console.log(globalLet); // Cannot access 'globalLet' before initialization
// console.log(globalConst); // Cannot access 'globalConst' before initialization
function globalFun() {
return 'globalFun';
}
var globalVar = 'globalVar';
let globalLet = 'globalLet';
const globalConst = 'globalConst';
// console.log(canNoGet); // canNoGet is not defined
function tmp() {
var canNoGet = "canNoGet";
}
/**
* 全域環境的註冊過程
*
* 是否為函式環境? 否
*
* 是函式或全域環境? 是
* 掃描目前的程式碼找出所有函式宣告
* 在其他函式之外註冊函式宣告:
* 註冊 globalFun, tmp
*
* 掃描目前的程式碼找出變數宣告
* 在函式式之外註冊 globalVar
* 在區塊之外註冊 globalLet, globalConst
*/
/* --------------------- */
函式環境的註冊過程
/* ------- function env -------------- */
function go(foo, bar) {
console.log(foo, bar, arguments);
doSomething(); // 'do something'
console.log(tmp); // tmp is not defined
function doSomething() {
var tmp = 'tmp';
let letTmp = 'letTmp';
const constTmp = 'constTmp';
console.log('do something');
}
/**
* 函式環境的註冊過程
*
* 是否為函式環境? 是
* 建立 arguments 物件與函式參數(foo, bar)
*
* 是函式或全域環境? 是
* 掃描目前的程式碼找出所有函式宣告
* 在其他函式之外註冊函式宣告
*
* 掃描目前的程式碼找出變數宣告
* 在函式之外註冊 var
* 在區塊之外註冊 let, const
*/
}
/* --------------------- */
區塊環境的註冊過程
/* --------- block scope env ------------ */
console.log(valInBlock); // undefined
// console.log(letInBlock); // letInBlock is not defined
// console.log(constInBlock); // constInBlock is not defined
// console.log(functionInBlock()); // functionInBlock is not a function
{
console.log(typeof functionInBlock); // "function"
console.log(functionInBlock()); // "functionInBlock"
var valInBlock = 'valInBlock';
let letInBlock = 'letInBlock';
const constInBlock = 'constInBlock';
/**
* 區塊環境的註冊過程
*
* 是否為函式環境? 否
*
* 是函式或全域環境? 否
*
* 是否為區塊環境? 是
* 掃描目前的程式碼找出 let, const 變數宣告
* 註冊 letInBlock, constInBlock
*/
}
/* --------------------- */
在函式執行期間,會跳過函式宣告,因此 fun 的定義對 fun 識別項沒有任何影響。
console.log(typeof fun); // 'function'
var fun = 3;
console.log(typeof fun); // 'number'
function fun(){}
console.log(typeof fun); // 'number'
Ref:
javascript - Lexical environment and function scope - Stack Overflow
JavaScript 全攻略:克服 JS 的奇怪部分 | Udemy
所有的函式都是閉包:談 JS 中的作用域與 Closure
secrets-of-the-javascript-ninja