對JavaScript中閉包的理解

在前端開發中閉包是一個很重要的知識點,是面試中必定會被問到的內容。以前我對閉包的理解主要是"經過閉包能夠在函數外部能訪問到函數內部的變量",對閉包運用的也不多,甚至本身寫過閉包本身都不太清楚,只知道這樣寫能夠解決問題。最近在梳理本身的js知識點,發現本身對js閉包理解的很不透徹,因而想全面的分析一下閉包,特別是閉包的造成緣由和閉包的使用場景。javascript

閉包的定義

閉包是指有權訪問另外一個函數做用域中的變量的函數 --《JavaScript高級程序設計》html

函數對象能夠經過做用域關聯起來,函數體內的變量均可以保存在函數做用域內,這在計算機科學文獻中稱爲「閉包」,全部的javascirpt函數都是閉包 --《Javascript權威指南》前端

看完這些專業的定義是否是感受一頭霧水,不要緊,我和同樣也沒明白這些定義說的是啥,咱接着往下分析。java

在認識閉包原理以前咱們先必須對做用域、執行上下文、執行上下文堆棧、變量對象、活動對象、做用域鏈有着全面的認識面試

做用域 Scope

做用域是一套規則,用於肯定在何處以及如何查找變量(標識符)
做用域共有兩種主要的工做模型:瀏覽器

  • 詞法做用域:做用域是在編寫代碼的時候肯定的
  • 動態做用域:做用域是在代碼運行的時候肯定的

咱們知道javascript使用的是詞法做用域緩存

執行上下文 Execution Contexts

Javascript中代碼的執行上下文分爲如下三種:安全

  • 全局級別的代碼 – 這個是默認的代碼運行環境,一旦代碼被載入,引擎最早進入的就是這個環境。
  • 函數級別的代碼 – 當執行一個函數時,運行函數體中的代碼。
  • Eval的代碼 – 在Eval函數內運行的代碼。

一個執行的上下文能夠抽象的理解爲一個對象。每個執行的上下文都有一系列的屬性(變量對象(variable object),this指針(this value),做用域鏈(scope chain) )閉包

Execution Contexts = {
    variable object:變量對象;
    this value: this指針;
    scope chain:做用域鏈;
}

執行上下文堆棧 Execution Contexts Stack

活動的執行上下文在邏輯上組成一個堆棧。堆棧底部永遠都是全局上下文(globalContext),而頂部就是當前(活動的)執行上下文。app

<script>
function add(num){
    var sum = 5;
    return sum + num;
}
var sum = add(4);
</script>

當add函數被調用時,add函數執行上下文被壓入執行上下文堆棧的頂端,此時執行上下文堆棧可表示爲:

EC Stack = [
  <add> functionContext
  globalContext
];

add函數執行完畢後,其執行上下文將會從執行上下文堆棧頂端彈出並被銷燬。全局執行上下文只有在瀏覽器關閉時纔會從執行上下文堆棧中銷燬

變量對象 Variable Object

若是變量與執行上下文相關,那變量本身應該知道它的數據存儲在哪裏,而且知道如何訪問。這種機制稱爲變量對象(variable object)。
能夠說變量對象是與執行上下文相關的數據做用域(scope of data) 。它是與執行上下文關聯的特殊對象,用於存儲被定義在執行上下文中的變量(variables)、函數聲明(function declarations) 。
當進入全局上下文時,全局上下文的變量對象可表示爲:

VO = {
    add: <reference to function>,
    sum: undefined,
    Math: <...>,
    String: <...>
    ...
    window: global //引用自身
}

活動對象 Activation Object

當函數被調用者激活時,這個特殊的活動對象(activation object) 就被建立了。它包含普通參數(formal parameters) 與特殊參數(arguments)對象(具備索引屬性的參數映射表)。活動對象在函數上下文中做爲變量對象使用。
當add函數被調用時,add函數執行上下文被壓入執行上下文堆棧的頂端,add函數執行上下文中活動對象可表示爲

AO = {
    num: 4,
    sum :5,
    arguments:{0:4}
}

做用域鏈 Scope Chain

函數上下文的做用域鏈在函數調用時建立的,包含活動對象AO和這個函數內部的[[scope]]屬性。

var x = 10;
function foo() {
  var y = 20;
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
  bar();
}
foo();

在這段代碼中咱們看到變量"y"在函數"foo"中定義(意味着它在foo上下文的AO中)"z"在函數"bar"中定義,可是變量"x"並未在"bar"上下文中定義,相應地,它也不會添加到"bar"的AO中。乍一看,變量"x"相對於函數"bar"根本就不存在;
函數"bar"如何訪問到變量"x"?理論上函數應該能訪問一個更高一層上下文的變量對象。實際上它正是這樣,這種機制是經過函數內部的[[scope]]屬性來實現的。
[[scope]]是全部父級變量對象的層級鏈,處於當前函數上下文之上,在函數建立時存於其中。

注意: [[scope]]在函數建立時被存儲是靜態的(不變的),直至函數銷燬。即:函數能夠永不調用,但[[scope]]屬性已經寫入,並存儲在函數對象中。
在這裏咱們逐步分析下
全局上下文的變量對象是:

globalContext.VO === Global = {
  x: 10
  foo: <reference to function>
};

在"foo"建立時,"foo"的[[scope]]屬性是:

foo.[[Scope]] = [
  globalContext.VO
];

在"foo"激活時(進入上下文),"foo"上下文的活動對象是:

fooContext.AO = {
  y: 20,
  bar: <reference to function>
};

"foo"上下文的做用域鏈爲:

fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];

內部函數"bar"建立時,其[[scope]]爲:

bar.[[Scope]] = [
  fooContext.AO,
  globalContext.VO
];

在"bar"激活時,"bar"上下文的活動對象爲:

barContext.AO = {
  z: 30
};

"bar"上下文的做用域鏈爲:

bar.Scope= [
  barContext.AO,
  fooContext.AO,
  globalContext.VO
];

介紹了一大堆內容,是否是有點暈忽忽的?堅持一下,下面是重點

閉包的原理

咱們經過一個閉包的例子來分析一下閉包的造成原理

function add(){
    var sum =5;
    var func = function () {
        console.log(sum);
    }
    return func;
}
var addFunc = add();
addFunc(); //5

js執行流進入全局執行上下文環境時,全局執行上下文可表示爲:

globalContext = {
    VO: {
        add: <reference to function>,
        addFunc: undefined
    },
    this: window,
    scope chain: window 
}

當add函數被調用時,add函數執行上下文可表示爲:

addContext = {
    AO: {
        sum: undefined //代碼進入執行階段時此處被賦值爲5
        func: undefined //代碼進入執行階段時此處被賦值爲function (){console.log(sum);}
    },
    this: window,
    scope chain: addContext.AO + globalContext.VO 
}

add函數執行完畢後,js執行流回到全局上下文環境中,將add函數的返回值賦值給addFunc。

因爲addFunc仍保存着func函數的引用,因此add函數執行上下文從執行上下文堆棧頂端彈出後並未被銷燬而是保存在內存中。

當addFunc()執行時,func函數被調用,此時func函數執行上下文可表示爲:

funcContext = {
    this: window,
    scope chain: addContext.AO + globalContext.VO 
}

當要訪問變量sum時,func的活動對象中未能找到,則會沿着做用域鏈查找,因爲js遵循詞法做用域,做用域在函數建立階段就被肯定,在add函數的活動對象中找到sum = 5;

介紹到這裏你明白造成閉包的緣由了嗎?
Javascript容許使用內部函數---即函數定義和函數表達式位於另外一個函數的函數體內。並且,這些內部函數能夠訪問它們所在的外部函數中聲明的全部局部變量、參數和聲明的其餘內部函數。當其中一個這樣的內部函數在包含它們的外部函數以外被調用時,就會造成閉包。

閉包的用途

閉包能夠用在許多地方。它的最大用處有兩個,一個是前面提到的能夠讀取函數內部的變量,另外一個就是讓這些變量的值始終保持在內存中。

1. 保護變量的安全實現JS私有屬性和私有方法

利用閉包能夠讀取函數內部的變量,變量在函數外部不能直接讀取到,從而達到保護變量安全的做用。由於私有方法在函數內部都能被訪問到,從而實現了私有屬性和方法的共享。
常見的模塊模式就是利用閉包的這種特性創建的

var Counter = (function() {
  //私有屬性
  var privateCounter = 0; 
  //私有方法
  function changeBy(val) { 
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();
console.log(privateCounter); //privateCounter is not defined 
console.log(Counter.value()); // 0
Counter.increment();
Counter.increment();
console.log(Counter.value()); // 2
Counter.decrement();
console.log(Counter.value()); // 1

在jQuery框架的私有方法和變量也是這麼設計的

var $ = jQuery = function(){
    return jQuery.fn.init();
}
jQuery.fn = jQuery.prototype = {
    init:function(){
        return this;  //this指向jQuery.prototype
    },
    length: 1,
    size: function(){
        return this.length;
    }
}
console.log($().size()); // 1

2. 將處理結果緩存

var mult = (function(){
    var cache = {};
    var calculate = function(){
        var a = 1;
        for(vari=0,l=arguments.length;i<l;i++){
            a = a*arguments[i];
        }
        return a;    
    };
    return function(){
        var args = Array.prototype.join.call(arguments,',');
        if(args in cache){
            return cache[args];
        }
        return cache[args] = calculate.apply(null,arguments);    
    }
})();

這樣咱們在第二次調用的時候,就會從緩存中讀取到該對象。

理解了閉包的原理咱們發現閉包的這些用途都是利用了閉包保存了當前函數的活動對象的特色,這樣閉包函數在做用域以外被調用時依然可以訪問其建立時的做用域

閉包的缺點

  • 閉包將函數的活動對象維持在內存中,過分使用閉包會致使內存佔用過多,因此在使用完後須要將保存在內存中的活動對象解除引用;
  • 閉包只能取得外部函數中任何變量的最後一個值,在使用循環且返回的函數中帶有循環變量時會獲得錯誤結果;
  • 當返回的函數爲匿名函數時,注意匿名函數中的this指的是window對象。

這裏僅僅是我對閉包的一些看法,如有錯誤的地方,還望你們提出,一塊兒交流共同進步!
參考文獻

相關文章
相關標籤/搜索