閉包全面詳解

什麼是閉包

最原始定義

閉包(closure),是指函數變量能夠保存在函數做用域內,所以看起來是函數將變量「包裹」了起來。閉包

//根據定義,包含變量的函數就是閉包
function foo() {
    var a = 0;
}
cosole.log(a) 
// Uncaught ReferenceError: a is not defined

《JavaScript高級程序設計》對閉包定義

閉包是指有權訪問另外一個函數做用域中的變量的函數函數

//訪問上層函數的做用域的內層函數就是閉包
function foo() {
    var a = 2;
    function bar() {
        console.log(a);
    }
    bar();
}
foo();

《JavaScript權威指南》對閉包定義

函數對象能夠經過做用域鏈相互關聯起來,函數體內部變量能夠保存在函數做用域內,這就是閉包。優化

var global = "global scope"; //全局變量
function checkscope() {
    var scope = "local scope"; //局部變量
    function f() {
        return scope; //在做用域中返回這個值
    };
    return f();
}
checkscope(); // 返回 "local scope"

《你不知道的JavaScript》這樣描述

當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行。設計

//fn3就是fn2函數自己。執行fn3能正常輸出name
//這不就是fn2能記住並訪問它所在的詞法做用域,並且fn2函數的運行仍是在當前詞法做用域以外了。
function fn1() {
    var name = 'iceman';
    function fn2() {
        console.log(name);
    }
    return fn2;
}
var fn3 = fn1();
fn3();

MDN 上面這麼說:

閉包是一種特殊的對象。它由兩部分構成:函數,以及建立該函數的環境。環境由閉包建立時在做用域中的任何局部變量組成。
簡單說就是指那些可以訪問自由變量的函數。code

嚴格來講,閉包須要知足三個條件:

【1】訪問所在做用域;
【2】函數嵌套;
【3】在所在做用域外被調用對象

閉包的造成原理

先了解JavaScript的垃圾回收機制

Javascript 會找出再也不使用的變量,再也不使用意味着這個變量生命週期的結束。
Javascript 中存在兩種變量——全局變量和局部變量,所有變量的聲明週期會一直持續,直到頁面卸載而局部變量聲明在函數中,它的聲明週期從執行函數開始,直到函數執行結束。在這個過程當中,局部變量會在堆或棧上被分配相應的空間以存儲它們的值,函數執行結束,這些局部變量也再也不被使用,它們所佔用的空間也就被釋放。
可是有一種狀況的局部變量不會隨着函數的結束而被回收,那就是局部變量被函數外部的變量所使用,其中一種狀況就是閉包,由於在函數執行結束後,函數外部的變量依然指向函數內的局部變量,此時的局部變量依然在被使用,因此也就不可以被回收生命週期

var scope = 'global scope';
function checkScope() {
    var scope = 'local scope';
    return function() {
        console.log(scope);
    }
}

var result = checkScope(); 
result();   // local scope checkScope變量對象中的scope,非全局變量scope

此匿名函數的做用域鏈包括checkScope的活動對象和全局變量對象, 當checkScope函數執行完畢後,checkScope的活動對象並不會被銷燬,由於匿名函數的做用域鏈還在引用checkScope的活動對象,也就是checkScope的執行環境被銷燬,可是其活動對象沒有被銷燬,留存在堆內存中,直到匿名函數銷燬後,checkScope的活動對象纔會銷燬ip

從做用域鏈理解閉包的造成

  1. 從理論角度:全部的函數。由於它們都在建立的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,由於函數中訪問全局變量就至關因而在訪問自由變量,這個時候使用最外層的做用域。
  2. 從實踐角度:如下函數纔算是閉包:

i. 即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回)
ii. 在代碼中引用了自由變量內存

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();

fContext = {//f函數的執行上下文
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

對的,就是由於這個做用域鏈,f函數在聲明的時候壓入了上層的變量對象,f 函數依然能夠讀取到 checkscopeContext.AO 的值,而且若是當 f 函數引用了 checkscopeContext.AO 中的值的時候,即便 checkscopeContext 被銷燬了,可是 JavaScript 依然會讓 checkscopeContext.AO 活在內存中(和垃圾回收機制有關下文會說),f 函數依然能夠經過 f 函數的做用域鏈找到它,正是由於 JavaScript 作到了這一點,從而實現了閉包這個概念。作用域

閉包的做用-模仿塊級做用域,封裝私有變量

任何在函數中定義的變量,均可以認爲是私有變量,由於不能在函數外部訪問這些變量。
私有變量包括函數的參數、局部變量和函數內定義的其餘函數。
function module() {
    var arr = [];
    function add(val) {
        if (typeof val == 'number') {
            arr.push(val);
        }
    }
    function get(index) {
        if (index < arr.length) {
            return arr[index]
        } else {
            return null;
        }
    }
    return {
        add: add,
        get: get
    }
}
var mod1 = module();
mod1.add(1);
mod1.add(2);
mod1.add('xxx');
console.log(mod1.get(2));//外部是沒法直接拿到arr的只能經過get來拿

閉包的做用-使變量保存在內存中不被銷燬

實例1-計數器

咱們來實現一個計數器,每調用一次計數器返回值加一:

var counter = 0;
function add() {
   return counter += 1;
}
add();
add();
add();// 計數器如今爲 3

問題:

  • 全局變量容易被其餘代碼改變
  • 若是我須要同時用兩個計數器,但這種寫法只能知足一個使用,另外一個還想用的話就要再寫個counter2函數,再定義一個counter2的全局變量。

那咱們把counter放在add函數裏面不就行了麼?

function add() {
    var counter = 0;
    return counter += 1;
} 
add();
add();
add();// 本意是想輸出 3, 但輸出的都是 1

因此這樣作的話,每次調用add函數,counter的值都要被初始化爲0,仍是達不到咱們的目的。

使用閉包來寫就會解決這些問題

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

// test
var addA = add() ;
var addB = add() ;
addA();        // 1
addA();        // 2
addB();        // 1
addB();        // 2

實例2-延時打印

這樣打印出來的所有都是10,緣由是for循環是同步的會在延時1000毫秒的過程當中一直執行
等function執行的時候變量i指向的是同一個內存地址,且值已經變成的10

for (var i = 1; i <= 10; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

改進,用自執行的函數建立簡單的閉包,讓每一次for循環的i都在不一樣的內存地址中且不被銷燬

for (var i = 1; i <= 10; i++) {
    (function () {
        var j = i;
        setTimeout(function () {
            console.log(j);
        }, 1000);
    })();
}

優化寫法

for (var i = 1; i <= 10; i++) {
    (function (j) {
        setTimeout(function () {
            console.log(j);
        }, 1000);
    })(i);
}

聯繫Static靜態變量

閉包的做用主要就是讓變量的值始終保持在內存中。
C++或C語言還有Java中都有static靜態變量也是讓變量始終保存在內存中。
這樣來看好像閉包好像有點static靜態變量的意思。

總結

閉包就是子函數能夠有權訪問父函數的變量、父函數的父函數的變量、一直到全局變量。 歸根結底,就是利用js得詞法(靜態)做用域,即做用域鏈在函數建立的時候就肯定了。 子函數若是不被銷燬,整條做用域鏈上的變量仍然保存在內存中,這樣就造成了閉包

相關文章
相關標籤/搜索