閉包是Javascript中最多見的語法和形式之一,閉包能夠避免全局污染,模塊化編程。javascript
要理解閉包,先要熟悉scope, Javascript的基本概念java
Javacript中的做用域有兩種jquery
定義在函數以內的變量處於local scope, 定義在函數以外的變量處於global scope, 函數每調用一次,就新生成一個scopeexpress
context和scope不一樣,scope決定變量的可見性,context則決定了this的值,在一樣的scope裏編程
context是能夠經過function methods修改的, .apply(), .bind(), .call()瀏覽器
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
'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 }
閉包就是內部函數訪問外部函數的變量
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:
例子:
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 }); });
這樣一個需求,給傳入的參數加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執行完畢
小結:若是咱們須要的函數絕大部分都相同,閉包經常是一個技巧
將內部的實現細節封裝起來,只暴露接口給外部調用,更新代碼,接口並不變化
示例:一個計數函數,每次調用都會+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變量的方法
閉包能夠訪問外部函數的變量,即便外部函數已經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
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
// 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/