函數做用域與閉包

函數做用域

要理解閉包,必須從理解函數被調用時都會發生什麼入手。javascript

咱們知道,每一個javascript函數都是一個對象,其中有一些屬性咱們能夠訪問到,有一些不能夠訪問,這些屬性僅供JavaScript引擎存取,是隱式屬性。[[scope]]就是其中一個。
[[scope]]就是咱們所說的做用域,其中存儲了執行期上下文的集合。因爲這個集合呈鏈式連接,咱們把這種鏈式連接叫作做用域鏈。前端

當函數被定義(建立)時有一個本身所在環境的做用域(GO全局做用域 ,如果在函數內部,就是引用別人的做用域),當函數被執行時,會將本身的獨一無二的AO(活動對象,是使用arguments和該函數內部的變量值初始化的活動對象)執行上下文放在前端,造成一個做用域鏈;當該函數執行完,本身的AO會被幹掉,回到被定義時的狀態。java

另外,變量的查找,就是找所在函數的做用域,首先從做用域的頂端開始查找,找不到的狀況下,會查找外部函數的活動對象,依次向下查找,直到到達做爲做用域鏈終點的全局執行環境。數組

下面看幾個查找變量例子,深刻理解函數做用域及做用域鏈。閉包

function a(){
   function b(){
      var b=2223;  
   }
   var a=78;
}
a()
b()
console.log(b)

輸出結果: error: b is not defined
當函數a執行完畢後,該函數內部的活動對象AO就會被銷燬。因此函數外部是訪問不到函數內部的變量的。模塊化


function outer(){
   function inner(){
      var b=2223;   
      a=0
   }
   var a=78;
   inner() //①
   console.log(a) 
   console.log(b)
}
outer()

輸出結果: 0 , error: b is not defined函數

當函數inner在被定義的階段,就會擁有(引用)函數outer的做用域(包括函數outer本身局部的活動對象AO和全局做用域);當函數inner()被執行的時候,會再建立一個本身的活動對象AO並被推入執行環境做用域鏈的前端。
inner()函數在被執行的時候,因爲變量a在outer()函數中已經存在並被inner()引用,因此inner()函數內部的變量a會修改掉外部函數變量a的值,而且能夠不用聲明。當inner()函數執行完畢後(執行到①處),inner()函數局部的AO會被銷燬,下面就訪問不到變量b了,並且這時候變量a的值將是被inner()函數修改過的值。學習


function a(){
  function b(){
     var b=2223;   
     a=0
  }
  var a=78;
  b=1
  b()
  console.log(b)
}
a()

輸出結果: 1this


var x=10;
function a(){
    console.log(x);
}
function b(){
    var x=20;
    a();
}
a();//10
b();//仍是10;

總之:函數在被定義階段,會引用着其所在環境的做用域;執行階段,會建立一個本身獨一無二的活動對象AO,並推入執行環境做用域鏈的前端;函數執行完畢以後,本身的執行上下文AO會被銷燬,因此,這時候訪問其內部的變量是訪問不到的。可是,閉包的狀況又有不一樣。code

簡述什麼是閉包

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

從理論的角度上,全部的JavaScript函數都是閉包,由於函數在被定義階段就會存儲一個本身所在環境的做用域,能夠訪問這個做用域中的全部變量。能夠說,閉包是 JS 函數做用域的副產品。理解js做用域,天然就明白了閉包,即便你不知道那是閉包。

從技術實踐的角度,如下函數纔算閉包:

  1. 定義該函數的執行環境的做用域(執行上下文)即便被銷燬 ,它的活動對象(AO)仍然會留在內存中,被該函數引用着。
  2. 引用了函數體外部的變量。

建立閉包常見方式,就是在一個函數A內部建立另外一個函數B,而後經過return這個函數B以便在外部使用,這個函數B就是一個閉包。

舉個例子:

//也算閉包
var a = 1;
function foo() {
    console.log(a);
}
foo();

//函數內部定義函數
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()()  //這裏至關於:
//var foo = checkscope();
//foo();

輸出結果:local scope
f()函數在被定義階段就被保存到了外部,這個時候就至關於外部的函數能夠訪問另外一個函數內部的變量,f()函數會造成一個閉包。

按照函數做用域的概念,當checkscope()執行完畢後,其局部的活動對象AO會被銷燬;可是因爲checkscope()函數執行完畢後返回一個函數,根據函數在被定義階段會引用該函數所在執行環境的執行上下文,被返回的函數f()即便被保存到了外部依然引用着checkscope()函數的執行期上下文,直到函數f()執行完畢,checkscope()函數的執行上下文才會被銷燬。

也就是說被嵌套的函數f()不管在什麼地方執行,都會包含着外部函數(定義該函數)的活動對象。因此,即便f()被保存到外部,也能夠訪問到另外一個函數checkscope()中定義的變量。

不管經過何種手段將內部函數傳遞到所在的詞法做用域之外, 它都會持有對原始定義做用域的引用, 不管在何處執行這個函數都會使用閉包。

由此看來,閉包可能會致使一個問題:致使原有做用域鏈不釋放,形成內存泄漏(內存空間愈來愈少)。能夠經過手動將被引用的函數設爲null,來解除對該函數的引用,以便釋放內存。

閉包的做用

  1. 實現公有變量。
function a(){
   var num=100;
   function b(){
      num++;
      console.log(num)
   }
   return b;
}

var demo=a();
demo();//101
demo();//102
function test(){
   var num=100;
   function a(){
      num++;
   }
   function b(){
      num--;
   }
   return [a,b]

}

var demo=test()
demo[0]();//101
demo[1]();//100
//函數a和函數b引用的是同一個做用域。
  1. 實現私有變量。

閉包一般用來建立內部變量,使得這些變量不能被外部隨意修改,同時又能夠經過指定的函數接口來操做。
經過在當即執行函數中return 將方法保存到外部等待調用,內部的變量因爲是私有的,外部訪問不到,可防止污染全局變量,利於模塊化開發。

var foo = ( function() { 
   var secret = 'secret'; 
   // 「閉包」內的函數能夠訪問 secret 變量,而secret變量對於外部倒是隱藏的 
   return { 
      get_secret: function () { 
         // 經過定義的接口來訪問 secret 
            return secret; 
      }, 
      new_secret: function ( new_secret ) { 
         // 經過定義的接口來修改 secret 
         secret = new_secret; 
      } 
   }; 
} () ); 
foo.get_secret (); // 獲得 'secret' 
foo.secret; // undefined,訪問不能 
foo.new_secret ('a new secret'); // 經過函數接口,咱們訪問並修改了secret 變量 
foo.get_secret (); // 獲得 'a new secret'
var name='bcd';
var init=(function (){
   var name='abc';
   function callName(){
      console.log(name);
   }

   //其餘方法

   return function () {
      callName();
      //其餘方法
   }
}())

init () //abc

閉包經典題

function createFunctions(){
  var result = new Array();
  for (var i=0; i < 10; i++){
    result[i] = function(){
      console.log(i);
     };
  }
return result;
}

var fun = createFunctions();
for(var i=0;i<10;i++){
    fun[i]();
}

輸出結果:打印十個10
數組每一個值都是一個函數,每一個函數對createFunctions()造成一個閉包,此時i都是引用createFunctions()中同一個i變量。

function test(){
   var arr=[];
   for(var i=0;i<10;i++){
      (function(j){
         arr[j]=function(){
            console.log(j);
         }
      }(i))
   }
   console.log(i)//10 ,i仍是10
   return arr
}
var myArr=test();
for(var i=0;i<10;i++){
   myArr[i]()
}

輸出結果:從0到9
此次依然把數組每一個值賦爲函數,不一樣的是循環十次當即執行函數,並將當前循環的i做爲參數傳進當即執行函數,因爲參數是按值傳遞的,這樣就把當前循環的i保存下來了。

閉包中的this對象

在閉包中使用this對象可能會致使一些問題,結果每每不是預想的輸出結果。
看個例子:

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

輸出結果:The Window

this對象是在運行時基於函數的執行環境綁定的:在全局函數中,this等於window,而當函數被做爲某個對象的方法調用時,this等於那個對象。匿名函數每每具備全局性,這裏能夠這樣理解,沒有任何對象調用這個匿名函數,雖然這個匿名函數擁有getNameFunc()的執行上下文。

由於這個匿名函數擁有getNameFunc()的執行上下文,經過把外部函數getNameFunc()做用域中的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

理解到這裏,基本上就搞定了閉包了。

學習資料

JavaScript 裏的閉包是什麼?應用場景有哪些?

相關文章
相關標籤/搜索