Closure

閉包可以保存函式內的變數,無法存函式外改變變數的值,並應用到巢狀函式內。

A couple of questions

// 函式使用外部變數,會使用最新的值嗎?
let name = "John";
function sayHi() {
  alert("Hi, " + name);
}
name = "Pete";
sayHi(); // what will it show: "John" or "Pete"?

// 函式會使用內部變數還是外部變數?
function makeWorker() {
  let name = "Pete";

  return function() {
    alert(name);
  };
}

let name = "John";

// create a function
let work = makeWorker();

// call it
work(); // what will it show? "Pete" (name where created) or "John" (name where called)?

Lexical Environment

在 JavaScript 裡,每個函式、{...}、整個程式碼都有內部物件叫 Lexical Environment。有 2 個部分

  • Environment Record 一個物件儲存所有區域變數當作屬性

  • 指向外部 lexical environment

變數只是一個 Environment Record 的屬性,取得或改變變數意思是取得或改變物件的屬性。

Function Declaration

函式在 Lexical Environment 被創造的時候就已經被宣告,變數是在執行到時才被創造。這是為什麼可以在函式宣告前使用。

Inner and outer Lexical Environment

當使用函式時,會創造 2 個 Lexical Environment, inner Lexical Environment 的屬性有帶入該函式的參數,outer Lexical Environment 有全域變數包含函式本身及用到的全域變數。當函式使用變數時,會先在內部尋找,然後尋找外部,然後是全域變數。在 "use strict" 下,變數未定億會出現錯誤,非 "use strict",會指定變數值為 undefined。

// 回答第 1 個問題,變數會使用最新的值,
let name = "John";

function sayHi() {
  alert("Hi, " + name);
}

name = "Pete"; // (*)

sayHi(); // Pete

Nested functions

function sayHiBye(firstName, lastName) {

  // helper nested function to use below
  function getFullName() {
    return firstName + " " + lastName;
  }

  alert( "Hello, " + getFullName() );
  alert( "Bye, " + getFullName() );

}

// 巢狀函式可以被返回作為新物件的屬性或直接作為結果。

// 巢狀函式作為新物件屬性
// constructor function returns a new object
function User(name) {

  // the object method is created as a nested function
  this.sayHi = function() {
    alert(name);
  };
}

let user = new User("John");
user.sayHi(); // the method "sayHi" code has access to the outer "name"

// 巢狀函式作為結果
function makeCounter() {
  let count = 0;

  return function() {
    return count++; // has access to the outer "count"
  };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
// 可以在 makeCounter() 外改變裡面的變數 count 嗎?
// 不行,count 是區域變數,無法從外部得到他的值
// 當執行 makeCounter() 多次,每次執行結果使獨立的嗎? count 相同嗎?
// 每次執行 makeCounter() 會產生新的 Lexical Environment,所以每次執行的結果互為獨立。

function makeCounter() {
  let count = 0;
  return function() {
    return count++;
  };
}

let counter1 = makeCounter();
let counter2 = makeCounter();

alert( counter1() ); // 0
alert( counter1() ); // 1

alert( counter2() ); // 0 (independent)

Environments in detail

1. 腳本開始時只有全域 Lexical Environment,裡面只有宣告 makeCounter(),所有函式宣告的時候,必然有一個隱藏的屬性 [[Environment]] 指向函式,

2. 呼叫函式的時候,創造 Lexical Environment,Environment Record 儲存區域變數,指向外部 lexical reference 被設定成 [[Environment]] 屬性的函式。

3. 執行到巢狀函式時,創造 [[Environment]] 屬性外部指向 makeCounter()。

4. makeCounter() 執行完畢,結果儲存在全域變數 counter,當 counter 被呼叫執行 return count++ 這行程式碼。

5. 雖然 makeCounter() 執行完畢,但仍有 [[Environment]] 屬性指向他,因此呼叫 counter() 時,會先創造一個空的 Lexical Environment 因為沒有參數,再來指向外部的 [[Environment]] 。

6. 呼叫 counter(),不只返回 count 值,也增加他的值。

開頭問題的解答:

Code blocks and loops, IIFE

閉包的特性可以應用在任何 {...} 上。

if

for

for (let i = 0; i < 10; i++) {
  // Each loop has its own Lexical Environment
  // {i: value}
}

alert(i); // Error, no such variable

Code blocks

// 當 2 個腳本有相同痊癒變數會出現問題,用 {...} 可以解決這樣的問題。
{
  // do some job with local variables that should not be seen outside

  let message = "Hello";

  alert(message); // Hello
}

alert(message); // Error: message is not defined

IIFE

// 在過去沒有 lexical environment,因此發明 immediately-invoked function expressions,
// 函式有區域變數並立即執行,

(function() {

  let message = "Hello";

  alert(message); // Hello

})();

// 沒有函式名稱
// Try to declare and immediately call a function
function() { // <-- Error: Unexpected token (

  let message = "Hello";

  alert(message); // Hello

}();

// 不能宣告同時執行
// syntax error because of parentheses below
function go() {

}(); // <-- can't call Function Declaration immediately

// Ways to create IIFE

(function() {
  alert("Parentheses around the function");
})();

(function() {
  alert("Parentheses around the whole thing");
}());

!function() {
  alert("Bitwise NOT operator starts the expression");
}();

+function() {
  alert("Unary plus starts the expression");
}();

Garbage collection

// 函式執行完,會被記憶體清除
function f() {
  let value1 = 123;
  let value2 = 456;
}

f();

// 用閉包的特性,函式執行完仍在記憶體內,[[Environment] 指向函式。
function f() {
  let value = 123;

  function g() { alert(value); }

  return g;
}

let g = f(); // g is reachable, and keeps the outer lexical environment in memory

// 函式被呼叫多次,相對應的 lexical environment,也會被儲存在記憶體。
function f() {
  let value = Math.random();

  return function() { alert(value); };
}

// 3 functions in array, every one of them links to Lexical Environment (LE for short)
// from the corresponding f() run
//         LE   LE   LE
let arr = [f(), f(), f()];

// 當全域變數被清除,函式消失
function f() {
  let value = 123;

  function g() { alert(value); }

  return g;
}

let g = f(); // while g is alive
// there corresponding Lexical Environment lives

g = null; // ...and now the memory is cleaned up

Real-life optimizations

// 實際狀況 JavaScript 引擎會進行優化,未作用的變數會被清除。
// v8 在 debug 時無法使用區域變數
function f() {
  let value = Math.random();

  function g() {
    debugger; // in console: type alert( value ); No such variable!
  }

  return g;
}

let g = f();
g();

// 會返回全域變數
let value = "Surprise!";

function f() {
  let value = "the closest value";

  function g() {
    debugger; // in console: type alert( value ); Surprise!
  }

  return g;
}

let g = f();
g();

Last updated