正確理解Javascript Closures -- 閉包

閉包是Javascript中最多見的語法和形式之一,閉包能夠避免全局污染,模塊化編程。javascript

要理解閉包,先要熟悉scope, Javascript的基本概念java

0 準備

Scope - 做用域

Javacript中的做用域有兩種jquery

  • Global scope
  • Local scope

定義在函數以內的變量處於local scope, 定義在函數以外的變量處於global scope, 函數每調用一次,就新生成一個scopeexpress

  • Global scope的生存期爲整個應用application
  • Local scope的生存期爲所在函數被調用和執行中

Context - 上下文

context和scope不一樣,scope決定變量的可見性,context則決定了this的值,在一樣的scope裏編程

context是能夠經過function methods修改的, .apply(), .bind(), .call()瀏覽器

 

Execution Context - 執行上下文

execution context就是execution scope,裏面的context和上面講到context不同🤣閉包

由於Javascript是一個單線程語言,因此同一時間只能執行一個任務,其他的任務會在execution context排隊等候。app

當Javascript interperter開始執行代碼,scope首先會被設爲global, global context會被添加到execution context,在那以後,每一次函數調用都會生成scope, 被添加到execution context中,須要注意的是,內部函數調用後,一樣會將新的scope添加到外部函數的execution context中,也就是說,每一個函數會生成它本身的execution context。curl

一旦當前context裏面的代碼執行完畢(瀏覽器執行),這個context會從execution context中popped off(出棧),當前context的狀態就會轉換成parent contextide

瀏覽器老是執行棧頂部的execution context,其實就是最內部的scope, 代碼的執行是從內而外

 

function parent () {
  child()
}

function child () {
  console.log('child')
}

parent()

 

圖1 execution context

 

Phase - 執行階段

execution context執行分爲兩個階段:creation phase, code execution phase

Creation Phase: 函數被調用可是尚未執行,這個階段主要作三件事

  • Creation of Variable (Activation) Object 變量對象生成
  • Creation of Scope Chain 做用域鏈生成
  • Setting of the value of context(this) 設置context(this)的值
'variableObject': {
    // contains function arguments, inner variable and function declarations
}

Scope Chain: 在variable object生成以後就會生成scope chain, scope chain 包含variable object,scope chain是用來解析變量的,當瀏覽器開始解析變量時,Javascript會從最裏層的代碼開始向外找,其實scope chain就是包含本身的execution context和父的execution context

'scopeChain': {
    // contains its own variable object and other variable objects of the parent execution contexts
}
executionContextObject = {
    'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts
    'variableObject': {}, // contains function arguments, inner variable and function declarations
    'this': valueOfThis
}

Code Execution Phase: 函數執行階段

 

1 什麼是閉包?

閉包就是內部函數訪問外部函數的變量

A closure is an inner function that has access to the outer (enclosing) function’s variables—scope chain.

當一個函數被建立後,它就能夠訪問建立它的scope,若是函數innerFunc是在另外一個函數outerFunc內部建立的,那麼innerFunc就能夠訪問建立它的outerFunc的scope, 即便outerFunc 執行結束了returns

 

示例:

function fnGenerator(str) {
    var stringToLog = 'The string I was given is "' + str + '"';
  
    return function() {
        console.log(stringToLog);
    }
}

var fnReturned = fnGenerator('Bat-Man');

fnReturned(); // -> The string I was given is "Bat-Man"

即便上面的fnGenerator執行完了,它的scope仍然在內存中,它返回的函數依舊能夠訪問fnGenerator的scope

 

閉包有三種scope chains: 

  • own scope (variables defined between its curly brackets)
  • outer function's variables (cannot call outer function's arguments object)
  • global variables

例子:

function showName (firstName, lastName) {

  ​var nameIntro = "Your name is ";
  // this inner function has access to the outer function's variables, including the parameter​
​  function makeFullName () {
        
​    return nameIntro + firstName + " " + lastName;
    
  }
​
  ​return makeFullName ();

}

​
showName ("Michael", "Jackson"); // Your name is Michael Jackson


jquery的例子:

$(function() {
​  var selections = []; 
  $(".niners").click(function() { // this closure has access to the selections variable​
    selections.push (this.prop("name")); // update the selections variable in the outer function's scope​
  });
});

 

2 閉包多用在哪些場景?

2.1 減小重複代碼

這樣一個需求,給傳入的參數加10, 或者20, 30...

function add10(num) {
		return num + 10;
}

function add20() {
  	return num + 20;
}

function add30() {
		return num + 30;
}

...

代碼看來有重複,怎麼解決呢?看看下面使用閉包來減小重複代碼

function addFactory(storedNum) {
    return function(num2) {
        return storedNum + num2;
    }
}


var add10 = addFactory(10);
var add20 = addFactory(20);
var add30 = addFactory(30);

console.log(add10(5)); // -> 15
console.log(add20(6)); // -> 26
console.log(add30(7)); // -> 37

 

addFactory 接收一個參數storedNum, 返回了一個函數,這個內部函數永久地保留了訪問storedNum的權限,並且內部函數接收一個參數,加在storedNum上

每一次調用addFactory,會生成一個scope, 裏面包含對傳入的參數storedNum的訪問權限,返回的函數能夠訪問這個scope,而且保留了對這個scope的訪問權限,即便addFactory執行完畢

 

小結:若是咱們須要的函數絕大部分都相同,閉包經常是一個技巧

 

2.2 隱藏數據(封裝)

將內部的實現細節封裝起來,只暴露接口給外部調用,更新代碼,接口並不變化

示例:一個計數函數,每次調用都會+1

function counterGenerator() {
    var counter = 1;
  
    return function() {
        return counter++;
    }
}

var incrementCounter = counterGenerator();
console.log(incrementCounter()); // -> 1
console.log(incrementCounter()); // -> 2
counter = 100; // <- sets a new global variable 'counter';
               // the one inside counterGenerator is unchanged
console.log(incrementCounter()); // -> 3

上面的代碼給調用者incrementCounter函數,隱藏了counterGenerator函數,incrementCounter是惟一操做counter變量的方法

 

 

3 閉包的特色

3.1 side effects - 邊界效應

閉包能夠訪問外部函數的變量,即便外部函數已經return

這是由於函數的執行使用的是同一個scope chain, 閉包內訪問了外部函數的變量,當函數返回時,閉包的context並無出棧,從而該函數的context也沒法出棧,這個scope chain一直存在

function celebrityName (firstName) {
    var nameIntro = "This celebrity is ";
    // this inner function has access to the outer function's variables, including the parameter​
   function lastName (theLastName) {
        return nameIntro + firstName + " " + theLastName;
    }
    return lastName;
}
​
​var mjName = celebrityName ("Michael"); // At this juncture, the celebrityName outer function has returned.​
​
​// The closure (lastName) is called here after the outer function has returned above​
​// Yet, the closure still has access to the outer function's variables and parameter​
mjName ("Jackson"); // This celebrity is Michael Jackson


 

3.2 閉包存儲的是外部函數變量的引用

function celebrityID () {
    var celebrityID = 999;
    // We are returning an object with some inner functions​
    // All the inner functions have access to the outer function's variables​
    return {
        getID: function ()  {
            // This inner function will return the UPDATED celebrityID variable​
            // It will return the current value of celebrityID, even after the changeTheID function changes it​
          return celebrityID;
        },
        setID: function (theNewID)  {
            // This inner function will change the outer function's variable anytime​
            celebrityID = theNewID;
        }
    }
​
}
​
​var mjID = celebrityID (); // At this juncture, the celebrityID outer function has returned.​
mjID.getID(); // 999​
mjID.setID(567); // Changes the outer function's variable​
mjID.getID(); // 567: It returns the updated celebrityId variable


 

2.3 循環更新外部函數的變量易出錯

// This example is explained in detail below (just after this code box).​
​function celebrityIDCreator (theCelebrities) {
    var i;
    var uniqueID = 100;
    for (i = 0; i < theCelebrities.length; i++) {
      theCelebrities[i]["id"] = function ()  {
        return uniqueID + i;
      }
    }
    
    return theCelebrities;
}
​
​var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}];
​
​var createIdForActionCelebs = celebrityIDCreator (actionCelebs);
​
​var stalloneID = createIdForActionCelebs [0];


console.log(stalloneID.id()); // 103

在上面函數的循環體中,閉包訪問了外部函數循環更新後的變量i,在stalloneID.id()執行前,i = 3,因此,結果爲103,要解決這個問題,可使用 Immediately Invoked Function Expression (IIFE)

function celebrityIDCreator (theCelebrities) {
    var i;
    var uniqueID = 100;
    for (i = 0; i < theCelebrities.length; i++) {
        theCelebrities[i]["id"] = function (j)  { // the j parametric variable is the i passed in on invocation of this IIFE​
           return uniqueID + j; // each iteration of the for loop passes the current value of i into this IIFE and it saves the correct value to the array​
           // returning just the value of uniqueID + j, instead of returning a function.​
        } (i); // immediately invoke the function passing the i variable as a parameter​
    }
​
    return theCelebrities;
}
​
​var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}];
​
​var createIdForActionCelebs = celebrityIDCreator (actionCelebs);
​
​var stalloneID = createIdForActionCelebs [0];

console.log(stalloneID.id); // 100​
​
​var cruiseID = createIdForActionCelebs [1];
console.log(cruiseID.id); // 101

 

通常狀況下,若是閉包訪問了外部循環變量,會和當即執行函數(immediately invoked function expression)結合使用,再看一個例子

for (var i = 0; i < 5; i++) {
    setTimeout(
      function() {console.log(i);},
      i * 1000
    );
}

結果是0,1,2,3,4秒後都log的是5,由於當i log的時候,循環已經執行完了,全局變量i變成了5

那麼怎麼讓每秒log的是0,1,2,3,4呢?能夠在IIFE裏使用閉包,將變量循環的值傳給當即執行函數

for (var i = 0; i < 5; i++) {
    setTimeout(
        (function(num) {
            return function() {
                console.log(num);
            }
        })(i),
        i * 1000
    );
}
// -> 0
// -> 1
// -> 2
// -> 3
// -> 4

咱們在setTimeout裏當即執行了匿名函數,傳遞了i給num, 閉包返回的函數將log num, 返回的函數將在setTimeout 0,1,2,3,4秒 後執行

 

參考資料:https://scotch.io/tutorials/understanding-scope-in-javascript#toc-scope-in-javascript

http://javascriptissexy.com/understand-javascript-closures-with-ease/

相關文章
相關標籤/搜索