JS閉包

在前端開發(fā)中閉包是一個很重要的知識點,是面試中一定會被問到的內(nèi)容。之前我對閉包的理解主要是"通過閉包可以在函數(shù)外部能訪問到函數(shù)內(nèi)部的變量",對閉包運用的也很少,甚至自己寫過閉包自己都不太清楚,只知道這樣寫可以解決問題。最近在梳理自己的js知識點,發(fā)現(xiàn)自己對js閉包理解的很不透徹,于是想全面的分析一下閉包,特別是閉包的形成原因和閉包的使用場景。

閉包的定義

閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù) --《JavaScript高級程序設(shè)計》

函數(shù)對象可以通過作用域關(guān)聯(lián)起來,函數(shù)體內(nèi)的變量都可以保存在函數(shù)作用域內(nèi),這在計算機科學(xué)文獻中稱為“閉包”,所有的javascirpt函數(shù)都是閉包 --《Javascript權(quán)威指南》

在認識閉包原理之前我們先必須對作用域、執(zhí)行上下文、執(zhí)行上下文堆棧、變量對象、活動對象、作用域鏈有著全面的認識

作用域 Scope

作用域是一套規(guī)則,用于確定在何處以及如何查找變量(標識符)

作用域共有兩種主要的工作模型:

  • 詞法作用域:作用域是在編寫代碼的時候確定的
  • 動態(tài)作用域:作用域是在代碼運行的時候確定的

我們知道javascript使用的是詞法作用域

執(zhí)行上下文 Execution Contexts

Javascript中代碼的執(zhí)行上下文分為以下三種:

  • 全局級別的代碼 – 這個是默認的代碼運行環(huán)境,一旦代碼被載入,引擎最先進入的就是這個環(huán)境。
  • 函數(shù)級別的代碼 – 當(dāng)執(zhí)行一個函數(shù)時,運行函數(shù)體中的代碼。
  • Eval的代碼 – 在Eval函數(shù)內(nèi)運行的代碼。

一個執(zhí)行的上下文可以抽象的理解為一個對象。每一個執(zhí)行的上下文都有一系列的屬性(變量對象(variable object),this指針(this value),作用域鏈(scope chain) )

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

執(zhí)行上下文堆棧 Execution Contexts Stack

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

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

當(dāng)add函數(shù)被調(diào)用時,add函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端,此時執(zhí)行上下文堆棧可表示為:

EC Stack = [
  <add> functionContext
  globalContext
];

add函數(shù)執(zhí)行完畢后,其執(zhí)行上下文將會從執(zhí)行上下文堆棧頂端彈出并被銷毀。全局執(zhí)行上下文只有在瀏覽器關(guān)閉時才會從執(zhí)行上下文堆棧中銷毀

變量對象 Variable Object

如果變量與執(zhí)行上下文相關(guān),那變量自己應(yīng)該知道它的數(shù)據(jù)存儲在哪里,并且知道如何訪問。這種機制稱為變量對象(variable object)。
可以說變量對象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域(scope of data) 。它是與執(zhí)行上下文關(guān)聯(lián)的特殊對象,用于存儲被定義在執(zhí)行上下文中的變量(variables)、函數(shù)聲明(function declarations)。
當(dāng)進入全局上下文時,全局上下文的變量對象可表示為:

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

活動對象 Activation Object

當(dāng)函數(shù)被調(diào)用者激活時,這個特殊的活動對象(activation object) 就被創(chuàng)建了。它包含普通參數(shù)(formal parameters) 與特殊參數(shù)(arguments)對象(具有索引屬性的參數(shù)映射表)?;顒訉ο笤诤瘮?shù)上下文中作為變量對象使用。

當(dāng)add函數(shù)被調(diào)用時,add函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端,add函數(shù)執(zhí)行上下文中活動對象可表示為

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

作用域鏈 Scope Chain

函數(shù)上下文的作用域鏈在函數(shù)調(diào)用時創(chuàng)建的,包含活動對象AO和這個函數(shù)內(nèi)部的[[scope]]屬性。

var x = 10;
function foo() {
  var y = 20;
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
  bar();
}
foo(); 
  • 在這段代碼中我們看到變量"y"在函數(shù)"foo"中定義(意味著它在foo上下文的AO中)"z"在函數(shù)"bar"中定義,但是變量"x"并未在"bar"上下文中定義,相應(yīng)地,它也不會添加到"bar"的AO中。乍一看,變量"x"相對于函數(shù)"bar"根本就不存在

  • 函數(shù)"bar"如何訪問到變量"x"?理論上函數(shù)應(yīng)該能訪問一個更高一層上下文的變量對象。實際上它正是這樣,這種機制是通過函數(shù)內(nèi)部的[[scope]]屬性來實現(xiàn)的。

  • [[scope]]是所有父級變量對象的層級鏈,處于當(dāng)前函數(shù)上下文之上,在函數(shù)創(chuàng)建時存于其中。

注意: [[scope]]在函數(shù)創(chuàng)建時被存儲是靜態(tài)的(不變的),直至函數(shù)銷毀。即:函數(shù)可以永不調(diào)用,但[[scope]]屬性已經(jīng)寫入,并存儲在函數(shù)對象中。

在這里我們逐步分析下
全局上下文的變量對象是:

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

在"foo"創(chuàng)建時,"foo"的[[scope]]屬性是:

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

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

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

"foo"上下文的作用域鏈為:

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

內(nèi)部函數(shù)"bar"創(chuàng)建時,其[[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執(zhí)行流進入全局執(zhí)行上下文環(huán)境時,全局執(zhí)行上下文可表示為:

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

當(dāng)add函數(shù)被調(diào)用時,add函數(shù)執(zhí)行上下文可表示為:

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

add函數(shù)執(zhí)行完畢后,js執(zhí)行流回到全局上下文環(huán)境中,將add函數(shù)的返回值賦值給addFunc。

由于addFunc仍保存著func函數(shù)的引用,所以add函數(shù)執(zhí)行上下文從執(zhí)行上下文堆棧頂端彈出后并未被銷毀而是保存在內(nèi)存中。

當(dāng)addFunc()執(zhí)行時,func函數(shù)被調(diào)用,此時func函數(shù)執(zhí)行上下文可表示為:

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

當(dāng)要訪問變量sum時,func的活動對象中未能找到,則會沿著作用域鏈查找,由于js遵循詞法作用域,作用域在函數(shù)創(chuàng)建階段就被確定,在add函數(shù)的活動對象中找到sum = 5;

Javascript允許使用內(nèi)部函數(shù)---即函數(shù)定義和函數(shù)表達式位于另一個函數(shù)的函數(shù)體內(nèi)。而且,這些內(nèi)部函數(shù)可以訪問它們所在的外部函數(shù)中聲明的所有局部變量、參數(shù)和聲明的其他內(nèi)部函數(shù)。當(dāng)其中一個這樣的內(nèi)部函數(shù)在包含它們的外部函數(shù)之外被調(diào)用時,就會形成閉包。

閉包的用途

閉包可以用在許多地方。它的最大用處有兩個,一個是前面提到的可以讀取函數(shù)內(nèi)部的變量,另一個就是讓這些變量的值始終保持在內(nèi)存中。

1. 保護變量的安全實現(xiàn)JS私有屬性和私有方法

利用閉包可以讀取函數(shù)內(nèi)部的變量,變量在函數(shù)外部不能直接讀取到,從而達到保護變量安全的作用。因為私有方法在函數(shù)內(nèi)部都能被訪問到,從而實現(xiàn)了私有屬性和方法的共享。
常見的模塊模式就是利用閉包的這種特性建立的

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框架的私有方法和變量也是這么設(shè)計的

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. 將處理結(jié)果緩存

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);    
  }
})();

這樣我們在第二次調(diào)用的時候,就會從緩存中讀取到該對象。

理解了閉包的原理我們發(fā)現(xiàn)閉包的這些用途都是利用了閉包保存了當(dāng)前函數(shù)的活動對象的特點,這樣閉包函數(shù)在作用域之外被調(diào)用時依然能夠訪問其創(chuàng)建時的作用域

閉包的缺點

  • 閉包將函數(shù)的活動對象維持在內(nèi)存中,過度使用閉包會導(dǎo)致內(nèi)存占用過多,所以在使用完后需要將保存在內(nèi)存中的活動對象解除引用;
  • 閉包只能取得外部函數(shù)中任何變量的最后一個值,在使用循環(huán)且返回的函數(shù)中帶有循環(huán)變量時會得到錯誤結(jié)果;
  • 當(dāng)返回的函數(shù)為匿名函數(shù)時,注意匿名函數(shù)中的this指的是window對象。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容