關於js 閉包的理解及特點

第一章、淺探閉包:

大致意思,在w函數內部,定義一個b內部函數,並用return返回,那麼b函數就是一個閉包函數。

function w(){
    var c = "開創獨立王國的閉包";
    var n = 1;
    function b() {
        console.log(c+'----打印'+n+'次');
        n++;
    }
    return b;
}
var f1 = w()

f1()//開創獨立王國的閉包----打印1次
f1()//開創獨立王國的閉包----打印2次

官方對閉包的定義:所謂“閉包”,指的是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。

結合以上示例,分析官方定義:
1、環境的表達式(通常是一個函數)—指的是函數b;
2、b擁有很多變量:c、n…;
3、b綁定了這些變量,比如b綁定了n;每次通過f1執行完b後,b的執行上下文n不消失,可以每次疊加。
4、這些變量也是該表達式的一部分:c、n確實是b函數的一部分。

閉包的意義在於:
js有兩個作用域:全局作用域,函數的局部作用域;
局部作用域內可以訪問全局作用域,但是全局作用域內卻無法訪問局部作用域。

var str = '我是全局作用域內的全局變量';

function f1(){
  var idx = '我是函數f1局部作用域內的變量';
  console.log('我在局部作用域內,訪問了全局變量str:---'+str);
}

console.log('我在全局作用域內,訪問了全局變量str:---'+str);

需求來了,我想在全局作用域內,訪問f1作用域的變量idx,如何做到呢?

這時候閉包就出現了。
我們可以在f1內定義一個函數,通過此函數去訪問f1;

var str = '我是全局作用域內的變量';
function f1(){
  var idx = '我是函數f1局部作用域內的變量';
  console.log('我在局部作用域內,訪問了全局變量str:---'+str);
  function funcb() {
    console.log('我是用來訪問idx的---'+idx)
  }
  return funcb
}
console.log('我在全局作用域內,訪問了全局變量str:---'+str);
var cc = f1();
cc();//訪問到了idx

看到沒有,我們在全局作用域內通過閉包函數funcb訪問到了局域變量idx;

這就是閉包的意義所在:訪問函數的私有變量。

閉包就好比國與國、星球與星球之間的通信。這一點後面去講解。

每個國與國之間都可以通過閉包的形式互相訪問變量。

因此理解閉包一定要深刻理解全局作用域、局部(函數)作用域、(執行)上下文的概念。
還要知道一個重要概念:javascript除了全局上下文之外,只有函數可以創建的函數執行上下文。
如果你沒有對以上四個概念有清晰的理解,請別說懂閉包。

閉包函數區別於普通函數的獨有特性在於一下兩點:
1、能訪問母函數內的變量,其他函數無法訪問母函數內變量。
例如:

function a() {
    var num = 10;
    return function (){
        num++;
        return num;
    }
}
var b = a();
console.log(num);//Uncaught ReferenceError: num is not defined

2、先看一段代碼:

//windows全局環境
var n=1;
function f1() {
  console.log(n);
  n++;
}
f1()//1
f1()//2

我們注意到,每次執行f1()後,n疊加1;再次執行f1(),n的值是上次函數執行的結果。也就是說n這個全局上下文不會被銷燬。
*這裏囉嗦一下,爲什麼全局上下文永遠不會銷燬,只有關閉程序是才銷燬呢。
這是js的特性:*
這裏寫圖片描述
上下文肯定是函數被執行時才產生的,那麼全局上下文是誰創建的呢,我們可以理解爲全局上下文是整個程序創建的,整個程序是最大的函數,猶如航空母艦,程序一旦執行,就會產生一個全局的上下文,只要程序處於執行中,這個全局上下文永遠不會消失,直至結束整個程序。
參考1
參考2
與全局上下文一樣,閉包函數的母函數執行後,母函數的上下文不銷燬,會被存到js內存中。,此特性是閉包的核心特性,也是理解閉包的根本所在,如果對閉包的此特性不甚瞭解,那就請看第二章,第二章將用一整章篇幅去解讀此特性。

function a() {
    var num = 10;
    return function (){
        num++;
        console.log(num);
    }
}
var b = a();
b()//11 ---執行完後,母函數a的上下文num應該要銷燬,重新回到num爲10。
b()//12 ---??居然是12,說明母函數a的上下文沒有被銷燬,這樣導致閉包函數每次執行時,其上下文都不一樣。

閉包函數的使用
所以閉包函數的引用,必須先執行母函數,母函數執行完後母函數的上下文不消失,閉包函數的母函數的上下文不消失,從而導致閉包函數每次執行時在同一個作用域內(母函數作用域內),閉包函數的上下文每次可能都不同。

閉包函數長什麼樣?
基於以上講解,具有閉包特性的函數(也就是閉包函數)是這樣的:
1、函數必須含有return 或可以返回函數;
2、函數必須return的是一個函數 或可以返回函數;
標準閉包函數示例一:

function a() {
    var num = 1;
    return function (){
        num++;
        console.log(num);
    }
}

標準閉包函數示例二:

function a(){
  var n = 0;
  this.inc = function () {
    n++;
    console.log(n);
  };
   this.ddf=function(){
    n+=5;
    console.log()
  }
}
var cc = new a();
cc.inc()
cc.ddf()

如上,一般可通過return或new的方式創建閉包。

閉包–國與國之間的通信或間諜

閉包的母函數a執行時,會創建一個函數w和一個母函數a上下文,因爲a返回的是閉包函數,所以可以將w看作是閉包函數,把這個母函數a上下文比作一個小國, 把window全局上下文比作一個大國的話,那麼這個函數w就是活動在大國裏面的一個小國的間諜, 函數w看似生活在大國的環境裏面,但實際上一切行動受小國控制, 所以函數w雖然活動於小國之外的大國window裏面, 但實際上函數w只能算生活在小國內的,與小國內的其他函數無異; 因此函數w的最高級作用域其實是小國,然後是外層的大國; 函數w無論在小國內或者window內活動,都能改變這兩國之間的變量或執行上下文; 所以閉包的設計原則目的之一是,通過閉包函數,遊離於window環境當中,去遙控改變閉包的母函數小國

var rr = 2;  
function a() {  
    var num = 10;  
    return function (){  
        num++;  
        rr++;  
        console.log(num);  
    }  
}  
var w = a();  
w();//遙控改變小國內的num,當然也可以改變window大國內的rr  
w();//12  

補充
任何函數,要去創建這個函數的作用域取值,而不是“父作用域”。理解了這一點,對閉包的理解有幫助。

var max = 10;
var num = 125;
function fn(x) {
    if(x>max){
        console.log(x)
    }
}

(function (f) {
    var max = 100;
    f(15)//15
    console.log(++num)//126
})(fn)

把函數看出一個變量,如本例的num變量,它的值由創建它的地方確定,同樣的fn的作用域也是創建它的地方去找。

王福朋寫了另外一種閉包形式,寫的比較精闢給出鏈接,可以瞭解

第二章、深探閉包:

整個js程序是最大的函數,程序一旦執行,就會產生一個全局的上下文,只要程序處於執行中,這個全局上下文永遠不會消失,直至結束整個程序。
在上面我們說了,閉包的母函數執行時,母函數產生了一個執行上下文,這個上下文類似一個windows的全局上下文。
全局上下文只有整個js程序刷新或重載時纔會銷燬原來上下文,並生產新的上下文;
當程序一旦執行後,全局上下文不銷燬。
正如全局上下文一樣。
母函數執行時,就相當於程序裝載,母函數執行生成上下文,就相當於整個程序裝載時產生的全局上下文。
當母函數一旦執行後,母函數的上下文就不會被銷燬。
爲什麼呢?

閉包函數綁架了母函數的上下文變量,從而導致了母函數的上下文不會被銷燬。

你問我爲什麼會有這樣的現象,我只能告訴你,這是js的遊戲規則。
就好比打籃球,你上籃被打手了就會得到罰球一樣,這是打籃球的規則;
這個規則也不是無中生有的,是合乎邏輯的。
你上籃上的好好的,別人打你的手,裁判判你一次罰球的機會,這合情合理吧。
回到js的遊戲中:
母函數創建了一個閉包函數,閉包函數引用(也可以說成是綁定、綁架)了母函數內的變量,那麼閉包函數每次執行的時候都需要引用母函數內的變量,這時候,js規定母函數的執行上下文不銷燬,時刻準備給閉包函數創建執行上下文,這合情合理吧。
所以你要記住js的這條名爲閉包的規則。
只需記住,閉包母函數的上下文永遠不會被銷燬,存在於內存當中。
讓我們再次重溫一次閉包的官方定義,是不是一下子覺得官方閉包的定義從未有過的貼切和準確:

所謂“閉包”,指的是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分

把閉包母函數的上下文看成全局上下文一樣去理解,問題就簡單了

javascript除了全局作用域之外,只有函數可以創建的作用域;
這是因爲js的作用域不是塊級作用域,而是函數式作用域。
js中與此類似的還有上下文的創建:
整個程序的執行產生全局上下文,局部函數的執行創建函數上下文;
相比全局上下文;
閉包函數的母函數產生的上下文也可看作一個全局上下文;
區別在於,
1、全局上下文是給所有函數用的;母函數上下文是給閉包函數用的;
2、全局上下文是js程序裝載時創建;母函數上下文是母函數執行時創建;

除此之外母函數上下文幾乎擁有全局上下文的一切屬性,例如:
二者都具有,一旦創建永遠不被銷燬,只有程序停止運行時,才銷燬。(母函數上下文也是永不銷燬哦);
作用域內的函數執行的上下文都基於對應的全局或母函數上下文:
比如全局上下文,對應的是全局作用域,全局作用域內的所有函數的上下文都基於全局上下文;
比如母函數上下文,對應的是母函數作用域,其作用域內的閉包函數的上下文是基於母函數上下文。

**我們把閉包母函數的上下文看成全局上下文一樣去理解,問題就簡單了。
瞭解了全局上下文的特質,就幾乎相當於瞭解了母函數上下文;**

得出結論

1、全局上下文永遠不會消失,直至結束整個程序;
2、母函數上下文也永不消失,因爲母函數也是程序的一部分,一旦母函數上下文被創建,上下文只會同母函數隨整個程序結束而結束。
3、js中有兩個全局上下文;window下的大的全局上下文;閉包函數母函數創建的小的全局上下文;
4、一個js程序,可以肯定會有一個全局上下文,可能沒有或有一個或多個母函數上下文;
5、當js中有閉包函數母函數時,因爲js中同時存在全局上下文和母函數上下文,因此會加重內存空間,影響性能;

小結語:

理解閉包函數的精髓全在於理解閉包母函數的上下文不銷燬。
而不銷燬的本質閉包函數綁定了母函數的域內變量,閉包函數的運行依賴於母函數的上下文。
然後是理解閉包函數執行上下文如何創建(壓棧)、銷燬(壓棧)。

函數壓棧出棧圖示:

普通函數:
如圖所示,普通函數每次從執行開始到最終結束都是重複如圖所示的過程;
這裏寫圖片描述

閉包函數:
如圖所示,閉包函數每次從執行開始到最終結束都是重複如圖所示紅色矩形框內的過程;
由此可見閉包函數的執行和結束只與母函數上下文有關,與全局上下文無瓜葛。
這裏寫圖片描述

容易誤解地方

容易誤解的地方,需要注意的是:
閉包函數的母函數上下文不銷燬,
但閉包函數自己的上下文每次都是銷燬的。

瞭解閉包,對上下文與作用域知識瞭解要透徹是關鍵,因此就寫了第三章。

第三章 上下文與作用域

上下文、作用域基本概念的比較:

1、作用域是聲明函數時就產生了,上下文是函數被執行才產生,並且函數執行完就消失。
2、作用域決定了上下文
3、作用域不會消失,無時不刻都存在着;上下文只有函數被執行時,才形成,函數執行完,上下文消失,但作用域沒有消失的概念,它更像是一個地盤;
4、在同一個作用域下,函數被執行時,可能會產生不同的上下文;
5、上下文是函數被執行時才產生的【這也是爲什麼上下文也叫執行上下文了,要帶執行二字】,執行完後一般會消失,但是閉包的函數父元素,執行完後,上下文不消失;

函數、上下文、作用域之間的聯繫:

1、函數只能改變上下文,而不能改變作用域範圍;
2、由於函數的作用域是函數被定義時就確定了的,函數不可能改變作用域,
因此函數一輩子都是在跟上下文打交道,每次的執行都是在改變和影響上下文。
相比作用域而言,與函數關係更加緊密的是上下文
3、
var str = 5;
上面這句代碼,
變量str 是函數的作用域;
變量str 的值是函數的上下文;
函數的作用域永遠都是str;
單str的值不一定永遠都是5;
這就說明了函數的作用域從一開始就確定並且永遠不會改變;
但函數的上下文卻可能時刻改變着。
4、其實我們大可以將作用域和上下文具體化理解:
作用域就是函數要用的一個個變量。
上下文就是函數執行時,每個變量的具體值。

上下文與作用域誤區

我們通常只關注作用域,但實際中,正如上面說的第二點,函數從被創建出來一直都是在依賴上下文,並改變和影響上下文。
所以相比作用域而言,函數與上下文關係更密切。
而我們往往只知道作用域,卻不太理解上下文。
我們其實更應該去理解js的上下文。

第四章、閉包的幾大特性:

本章講述的這些特性,並非是都是閉包特有的,其他函數可能也有。

閉包函數是內部函數,因此繼承了內部函數的一些特性:

1、可以訪問父級函數的變量

上面的示例就是例子

2、閉包函數內的this指向window

var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
        return this.name;
      };
    }
  };
  alert(object.getNameFunc()());//The Window

3、不能直接訪問父級函數的this和arguments

閉包函數,也是一個內部函數,內部函數不能直接訪問父級函數的this和arguments;可以通過在父級函數內將this和arguments賦值給變量,再通過訪問父級函數變量的方式訪問。

//不能直接訪問父級函數的this
var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
        return this.name;
      };
    }
  };
  alert(object.getNameFunc()());//The Window
//將父級函數的this賦值給that變量,通過that訪問父級函數的this
var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };
    }
  };
  alert(object.getNameFunc()());//My Object

4、閉包函數使用的變量會被存入內存

閉包父級函數在被引用執行時,產生的上下文環境會被存入內存,不會隨着執行結束而被銷燬。
也就是說,上下文環境中包含的變量會被存入內存,不會隨調用結束而被銷燬,只有刷新或關閉瀏覽器才銷燬。
上下文環境的理解,見鏈接

function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();//父級函數被執行,產生上下文環境{n:999}
  //父級函數被執行完,它產生的上下文環境{n:999}不銷燬
  result(); // 999
  nAdd();//父級函數產生的上下文環境被改變,{n:1000}
  result(); // 1000

第五章、難點閉包示例一:

function w(){
    var c = 8;
    console.log("w被執行了")
    function b() {
        console.log(c);
        c++;
    }
    return b;
}
w()()//8
w()()//8
w()()//8
var d = w();
d()//8
d()//9
d()//10

將w()()拆開成var d = w();d();運行後,執行結果爲什麼不一樣?
這個問題很簡單,我們分析w()():
在w()()中,w()創建了上下文,這個上下文就是w()()的執行上下文,因此打印爲8;
當執行到第二個w()()時,w()創建了上下文,這個上下文就是本行w()()的執行上下文,因此打印也爲8;

我們分析:

var d = w();
d()//8
d()//9
d()//10

這裏在第一句引用了w(),w()創建了上下文。
在下面幾句中,d()是閉包函數的執行,並沒有執行w();所以下面三句用的上下文環境都是var d = w()這句創造的同一個上下文環境。

綜上:
將w()()拆開成var d = w();d();運行後,兩種方式執行結果爲什麼不一樣?
因爲這兩種方式是有區別的,第一種方式執行了三次w,第二種方式執行了一次w

第六章、難點閉包示例二:

var a =function(y){
    var x=y;
    console.log("匿名函數運行了")
    return function(){
        console.log(x);
        x++;
        console.log(y);
        y--;
    }
}(5);
a();//5 5
a();//6 4
a();//7 3

本例理解的難點是,示例中的匿名函數爲什麼會自運行了,原來

var a = function(x){}(n)

是匿名函數可以自運行的形式之一,相當於(function (x) {})(n)

關於匿名函數自運行的十三種方式,參考鏈接

( function() {}() );
( function() {} )();
[ function() {}() ];

~ function() {}();
! function() {}();
+ function() {}();
- function() {}();

delete function() {}();
typeof function() {}();
void function() {}();
new function() {}();
new function() {};

var f = function() {}();//本例這裏

1, function() {}();
1 ^ function() {}();
1 > function() {}();

有人覺得,可以上本示例的變量a改造成如下,注意這是一種錯誤的函數聲明方法,這種方法不報錯,但(5)在此無任何意義,不會被瀏覽器解析,不信,你可以打印a,打印出來的函數,沒有(5):

function a(y){
    var x=y;
    console.log("匿名函數運行了")
    return function(){
        console.log(x);
        x++;
        console.log(y);
        y--;
    }
}(5);

爲什麼不能這樣改造呢,其實示例中var a = 的右邊是一個自運行的匿名函數,不是一個普通靜態的匿名函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章