這是我參與8月更文挑戰的第4天,活動詳情查看:8月更文挑戰javascript
理解閉包,首先必須理解變量做用域,在ECMAScript5的標準中有兩種做用域:全局做用域和函數做用域。 二者的調用關係是:java
let num = 1;
function test() {
let n = 2;
console.log(num); // 1
}
test();
console.log(n); // ReferenceError: n is not defined
複製代碼
實際開發中會出於各類緣由,咱們必須得拿到函數內部的局部變量。數組
JavaScript 語言規定:父對象的全部變量,對子對象都是可見的,反之則不成立。即"鏈式做用域"結構(chain scope) 。 基於這一點,咱們就能夠在目標函數內再定義一個函數,這個子函數就能夠正常訪問其父函數的內部變量。緩存
function parent() {
let n = 1;
function child() {
console.log(n); // 1
}
}
複製代碼
既然子函數能夠拿到父函數的局部變量,那麼父函數直接返回這個子函數,不就達到了在全局做用域下訪問函數內部變量的目的了。安全
function parent() {
let n = 1;
function child() {
console.log(n); // 1
};
return child;
}
let f1 = parent();
f1();
複製代碼
上述的例子就是一個最簡單的閉包的寫法:函數 child 就是閉包,因此閉包就是一個「定義在函數內部的函數」。 在本質上,閉包就是一座鏈接函數內外的橋樑。markdown
閉包自己還具備如下幾點重要的特性:閉包
3.1 函數做爲返回值異步
上述的例子還能夠進一步精簡爲匿名函數的寫法: 經過匿名函數訪問其外層函數的內部變量 num,而後外層函數返回該匿名函數,該匿名函數繼續返回 num 變量。函數
function closure1(){
let num = 1;
return function(){
return num
}
}
let fn1 = closure1();
console.log(fn1()); // 1
複製代碼
這樣就能夠在全局做用域下聲明一個變量 fn1 來承接 num 變量,這樣就達到了在全局做用域訪問函數內局部變量的目的。oop
3.1.1 保存變量 閉包在能夠讀取函數內局部變量的同時,它還可讓這些變量始終保存在內存中,不會在函數調用結束後,被垃圾回收機制回收。 好比這個例子:
function closure2(){
let num = 2;
return function(){
let n = 0;
console.log(n++,num++);
}
}
let fn2 = closure2();
fn2(); // 0 2
fn2(); // 0 3
複製代碼
執行兩次函數實例 fn2(),能夠看到結果是略有差別的:
變量 n 是匿名函數的內部變量,在匿名函數調用結束後,它這塊內存空間就會被正常釋放,即被垃圾回收機制回收。
匿名函數內引用了其外層函數的局部變量 num,即便匿名函數的調用結束了,可是這種依賴關係依然存在,因此變量 num 就沒法被銷燬。一直保存在內存中 匿名函數下次調用時,就會繼續沿用上次的調用結果。
利用閉包的這一特性,確實能夠作簡單的數據緩存。 可是也不能濫用閉包,這樣很容易使內存消耗增大,進而致使內存泄漏或者網頁的性能問題。
3.1.2 多個閉包函數彼此獨立
同一個閉包機制能夠建立出多個閉包函數實例,它們彼此獨立,互不影響。
好比下面這個簡單的例子:
function fn(num){
return function(){
return num++
}
}
複製代碼
咱們分別聲明三個閉包函數實例,分別傳入不一樣的參數。而後分別執行1,2,3次:
function fn(num){
return function(){
return num++
}
}
let f1 = fn(10);
let f2 = fn(20);
let f3 = fn(30);
console.log(f1()) // 10
console.log(f2()) // 20
console.log(f2()) // 21
console.log(f3()) // 30
console.log(f3()) // 31
console.log(f3()) // 32
複製代碼
能夠看到:f1(),f2(),f3()的第一次執行依次輸出了10 20 30,多執行的也是在自身上次執行的結果上累加的,互相之間沒有影響。
3.2 當即執行函數(IIFE)
上一種寫法中函數只是做爲返回值返回,而具體的函數調用是寫在其餘地方。那麼咱們能不能讓外層函數直接返回閉包的調用結果呢?
答案固然是能夠的:採用當即執行函數(IIFE)的寫法。
接下來就先了解一下具體什麼是當即執行函數(IIFE):
咱們都知道,在 JavaScript中調用函數最經常使用的方法就是函數名以後跟圓括號()。有時,咱們須要在定義函數以後,當即調用該函數。可是你不能直接在函數定義以後加上圓括號,這樣會產生語法錯誤。
// 提示語法錯誤
function funcName(){}();
複製代碼
產生錯誤的緣由是,function 關鍵字既能夠看成語句,也能夠看成表達式。
// 語句
function f() {}
// 表達式
var f = function f() {}
複製代碼
看成表達式時,函數能夠定義後直接加圓括號調用。
var f = function f(){ return 1}();
console.log(f) // 1
複製代碼
爲了不解析的歧義,JavaScript 規定,若是 function 關鍵字出如今行首,一概解釋成語句。那麼若是咱們還想用 function 關鍵字聲明函數後能當即調用,就須要讓 function 不直接出如今行首,讓引擎將其理解成一個表達式。 最簡單的處理,就是將其放在一個圓括號裏面。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
複製代碼
這就叫作「當即調用的函數表達式」(Immediately-Invoked Function Expression),即當即執行函數 簡稱IFE 。
3.2.1 定時器 setTimeout 的經典循環輸出問題
瞭解過當即執行函數後,趕忙來看一個實例:使用for循環依次輸出1~5。那麼若是是下面的代碼,它的運行結果是什麼?
for (var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i); // 6 6 6 6 6
}, 1000 );
}
複製代碼
結果確定是輸出5個6。緣由是 for 循環屬於同步任務,setTimeout 定時器屬於異步任務的宏任務範疇。JavaScript 引擎會優先執行同步的主線程代碼,再去執行宏任務。
因此在執行 setTimeout 定時器以前,for 循環就已經結束了,此時循環變量 i = 6。而後 setTimeout 定時器被循環建立了 5 次,所有執行完畢也就輸出了5個6。
可是咱們的目的是但願輸出1~5,這樣顯然沒達到要求。在正式介紹當即執行函數(IIFE)的寫法以前,我先說另一種方法:循環變量 i 使用let關鍵字聲明。
for (let i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i); // 1 2 3 4 5
}, 1000 );
}
複製代碼
爲何換成let聲明以後就能夠呢?是由於要想實現1~5循環輸出的本質要求是記住每次循環時循環變量的值。
而 let 的聲明方式剛好就能夠知足。傳送門:JavaScript中 var、let、const 特性及區別詳解
這樣再來看當即執行函數(IIFE)的寫法:
for (var i = 1; i <= 5; i++) {
(function(i){
setTimeout( function timer() {
console.log(i); // 1 2 3 4 5
}, 1000 );
})(i);
}
複製代碼
把 setTimeout 定時器函數用一個外層匿名函數包裹構成閉包的形式,而後再採用當即執行函數(IIFE)的寫法:繼續用圓括號包裹外層匿名函數,而後跟上圓括號調用,並把每次的循環變量做爲參數傳入。 這樣每次循環的結果就是閉包的調用結果:輸出 i 的值;再根據閉包自己的特性之一:能夠保存變量或參數,就知足了全部條件從而正確輸出了1~5。
再多說一點,目前的輸出形式是一秒後同時輸出1~5;那我想這五個數字每隔一秒再輸出一個呢?
for (var i = 1; i <= 5; i++) {
(function(i){
setTimeout( function timer() {
console.log(i);
}, i*1000 );
})(i);
}
複製代碼
能夠控制每一個setTimeout定時器的第二個參數:間隔時長,依次乘上循環變量 i 便可。 效果以下:
3.2.2 函數做爲API的形參傳入
閉包結合當即執行函數(IIFE) 的這種機制還有一類很重要的用處是:須要函數做爲形參的各類API。 以數組的 sort() 方法爲例:Array.prototype.sort() 方法中支持傳入一個比較器函數,來讓咱們自定義排序的規則。該比較器函數必需要有返回值,推薦返回 Number 類型。
好比如下的數組場景:咱們但願你能編寫一個 mySort() 方法:能夠按照指定的任意屬性值降序排列數組元素。 mySort() 方法確定須要兩個形參:須要排序的數組 arr 和指定的屬性值 property。
另外用到的 API 確定仍是 sort() 方法,這裏咱們就不能直接傳入一個比較器函數,而是採用閉包的IIFE寫法: 屬性值 property 做爲參數傳入外層匿名函數,而後匿名函數內部返回最終 sort() 方法須要的比較器函數。
var arr = [
{name:"code",age:19,grade:92},
{name:"zevi",age:12,grade:94},
{name:"jego",age:15,grade:95},
];
function mySort(arr,property){
arr.sort((function(prop){
return function(a,b){
return a[prop] > b[prop] ? -1 : a[prop] < b[prop] ? 1 : 0;
}
})(property));
};
mySort(arr,"grade");
console.log(arr);
/* [ {name:"jego",age:15,grade:95}, {name:"zevi",age:12,grade:94}, {name:"code",age:19,grade:92}, ] */
複製代碼
3.3 封裝對象的私有屬性和私有方法
閉包同時也能夠用於對象的封裝,尤爲是封裝對象的私有屬性和私有方法:
咱們封裝了一個對象 Person,它擁有一個公共屬性 name,一個私有屬性 _age 和兩個私有方法。 咱們不能直接訪問和修改私有屬性 _age,必須經過調用其內部的閉包 getAge 和 setAge。
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = Person('zevin');
p1.setAge(22);
console.log(p1.getAge()); // 22
複製代碼
4.1 優勢
4.1.1 實現封裝,保護函數內的變量安全
採用閉包的寫法能夠把變量保存在內存中,不會被系統的垃圾回收機制銷燬,從而起到了保護變量的做用。
function closure2(){
let num = 1;
return function(){
console.log(num++)
}
}
let fn2 = closure2();
fn2(); // 1
fn2(); // 2
複製代碼
4.1.2 避免全局變量的污染
開發中應該儘可能避免使用全局變量,防止沒必要要的命名衝突和調用錯亂
// 報錯
var num = 1;
function test(num){
console.log(num)
}
test();
let num = test(4);
console.log(num);
複製代碼
這時就能夠選擇把變量聲明在函數內部,並採用閉包的機制。
這樣既能保證變量的正常調用,又能夠避免全局變量的污染。
function test(){
let num = 1;
return function(){
return num
}
}
let fn = test();
console.log(fn());
複製代碼
4.2 缺點
4.2.1 內存消耗和內存泄漏
外層函數每次運行,都會生成一個新的閉包,而這個閉包又會保留外層函數的內部變量,因此內存消耗很大。
解決方法:不濫用閉包。
同時閉包中引用的內部變量會被保存,得不到釋放,從而也形成了內存泄漏的問題。
解決方法:
window.onload = function(){
var userInfor = document.getElementById('user');
var id = userInfor.id;
oDiv.onclick = function(){
alert(id);
}
userInfor = null;
}
複製代碼
在內部閉包使用變量 userInfor 以前,先用一個其餘的變量id 來承接一下,而且使用完變量 userInfor 後手動爲它賦值爲 null。