JavaScript Closures (閉包)

前言

如果你耐心地閱讀完這篇文章舷蒲,你將會(huì)了解到閉包的定義、用法盏求、優(yōu)點(diǎn)以及缺點(diǎn)噪漾!

簡單來說:Closures(閉包)是一個(gè)函數(shù)的作用域
通俗來說:Closures(閉包)是使用被作用域封閉變量充活,函數(shù)蜂莉,閉包等執(zhí)行的一個(gè)函數(shù)的作用域

閉包是一個(gè)函數(shù)和聲明該函數(shù)的詞法環(huán)境的組合,從理論角度來說混卵,所有有函數(shù)都是閉包

閉包的好處:提供了面向?qū)ο缶幊痰脑S多優(yōu)點(diǎn)
尤其是數(shù)據(jù)隱藏數(shù)據(jù)封裝

閉包的缺點(diǎn): 內(nèi)存泄漏映穗,影響處理速度和內(nèi)存消耗

本文摘要

  • 詞法作用域
  • 閉包
  • 實(shí)用的閉包
  • 用閉包模式模擬私有方法(模塊化)
  • 在循環(huán)中創(chuàng)建閉包:一個(gè)常見錯(cuò)誤
  • 性能考量

正文

詞法作用域

function init(){

 //name是一個(gè)被init創(chuàng)建的局部變量
  let name = "js";

//displayName()是一個(gè)內(nèi)部函數(shù)
 function displayName(){
    //一個(gè)閉包使用父函數(shù)中聲明的變量
     console.log(`name=${name}`);
   }
  displayName();
}
init()

函數(shù)init()創(chuàng)建了一個(gè)局部變量name和一個(gè)名為displayName()的函數(shù)
displayName()是一個(gè)內(nèi)部函數(shù)--定義于init()之內(nèi)且僅在該函數(shù)體內(nèi)可用
displayName沒有任何自己的局部變量,然而它可以訪問外部函數(shù)的變量幕随,即可以使用父函數(shù)init()中聲明的name變量

詞法作用域

閉包

現(xiàn)在來考慮如下的例子

function makeFunc() {
    let name = "js";
    function displayName() {
        console.log(`name = ${name}`); 
    }
    return displayName;
}

let myFunc = makeFunc();
myFunc();

運(yùn)行這段代碼的效果和之前的init()實(shí)例完全一樣蚁滋,字符串'js'將會(huì)打印在控制臺(tái),其中的不同-也是有意思的地方-在于displayName()內(nèi)部函數(shù)在執(zhí)行前從其外圍函數(shù)返回了

這段代碼看起來別扭卻能正常運(yùn)行赘淮。在一些編程語言中辕录,函數(shù)中的局部變量僅在函數(shù)的執(zhí)行周期可用。一旦makeFunc()執(zhí)行過后梢卸,我們會(huì)很合理地認(rèn)為name變量將不再可用走诞,然后,js中并不是這樣的蛤高?

這個(gè)謎底的答案是myFunc 變成了一個(gè)閉包蚣旱。閉包是一種特殊的對(duì)象碑幅。它由兩部分構(gòu)成:函數(shù),以及創(chuàng)建該函數(shù)的環(huán)境姻锁。環(huán)境由閉包創(chuàng)建時(shí)在作用域中的任何局部變量組成枕赵。在我們的例子中,myFunc是一個(gè)閉包位隶,由displayName函數(shù)和閉包創(chuàng)建時(shí)存在的“js” 字符串形成

閉包構(gòu)成

簡單來說:
閉包的構(gòu)成:函數(shù),以及創(chuàng)建該函數(shù)的環(huán)境(如圖所示)
環(huán)境:由閉包創(chuàng)建時(shí)在作用域中的任何局部變量組成

實(shí)用的閉包

理論就是這些了 — 可是閉包確實(shí)有用嗎开皿?讓我們看看閉包的實(shí)踐意義涧黄。閉包允許將函數(shù)與其所操作的某些數(shù)據(jù)(環(huán)境)關(guān)連起來。這顯然類似于面向?qū)ο缶幊谈尘!T诿嫦驅(qū)ο缶幊讨兴裢祝瑢?duì)象允許我們將某些數(shù)據(jù)(對(duì)象的屬性)與一個(gè)或者多個(gè)方法相關(guān)聯(lián)。

因而窄潭,一般說來春宣,可以使用只有一個(gè)方法的對(duì)象的地方,都可以使用閉包

大部分我們所寫的 Web JavaScript 代碼都是事件驅(qū)動(dòng)的 — 定義某種行為嫉你,然后將其添加到用戶觸發(fā)的事件之上(比如點(diǎn)擊或者按鍵)月帝。我們的代碼通常添加為回調(diào):響應(yīng)事件而執(zhí)行的函數(shù)
以下是一個(gè)實(shí)際的示例:假設(shè)我們想在頁面上添加一些可以調(diào)整字號(hào)的按鈕。一種方法是以像素為單位指定 body 元素的 font-size幽污,然后通過相對(duì)的 em 單位設(shè)置頁面中其它元素(例如頁眉)的字號(hào):

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}

我們的交互式的文本尺寸按鈕可以修改 body 元素的 font-size 屬性嚷辅,而由于我們使用相對(duì)的單位,頁面中的其它元素也會(huì)相應(yīng)地調(diào)整距误。

以下是 JavaScript:

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

size12簸搞,size14size16為將body 文本相應(yīng)地調(diào)整為 12,14准潭,16 像素的函數(shù)趁俊。我們可以將它們分別添加到按鈕上(這里是鏈接)。如下所示:

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>

用閉包模擬私有方法

諸如Java在內(nèi)的一些語言支持將方法聲明為私有的刑然,即他們只能被用一個(gè)類中的其他方法所調(diào)用

對(duì)此寺擂,javascript并不提供原生的支持,但是可以使用閉包模擬私有方法闰集。私有方法不僅僅有利于限制對(duì)代碼的訪問沽讹;還提供了管理全局變量命名空間的強(qiáng)大能力,避免非核心的方法弄亂了代碼的公共接口部分

下面的實(shí)例展示了如果使用閉包來定義公共函數(shù)武鲁,且其可以訪問私有函數(shù)和變量爽雄。這個(gè)方式稱為模塊模式

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(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

這里有很多細(xì)節(jié)。在以往的示例中沐鼠,每個(gè)閉包都有它自己的環(huán)境挚瘟;而這次我們只創(chuàng)建了一個(gè)環(huán)境叹谁,為三個(gè)函數(shù)所共享:Counter.increment, Counter.decrement, Counter.value

該共享環(huán)境創(chuàng)建于一個(gè)匿名函數(shù)體內(nèi),該函數(shù)一經(jīng)定義立即執(zhí)行乘盖。環(huán)境中包含兩個(gè)私有項(xiàng):名為 privateCounter 的變量和名為changeBy 的函數(shù)焰檩。這兩項(xiàng)都無法再匿名函數(shù)外部直接訪問。必須通過匿名包裝器返回的三個(gè)公共函數(shù)訪問

你應(yīng)該注意到了订框,我們定義了一個(gè)匿名函數(shù)用于創(chuàng)建計(jì)數(shù)器析苫,然后直接調(diào)用該函數(shù),并將返回值賦值給Counter變量穿扳。也可以將這個(gè)函數(shù)保存到另一個(gè)變量中衩侥,以便創(chuàng)建多個(gè)計(jì)數(shù)器。

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

請(qǐng)注意兩個(gè)計(jì)數(shù)器是如何維護(hù)他們各自的獨(dú)立性的矛物。每次調(diào)用makeCounter()函數(shù)期間茫死,其環(huán)境是不同的。每次調(diào)用中履羞,privateCounter 中含有不同的實(shí)例

簡單來說:
閉包可以提供面向?qū)ο缶幊痰脑S多優(yōu)點(diǎn)
1.數(shù)據(jù)隱藏
2.數(shù)據(jù)封裝

在循環(huán)中創(chuàng)建閉包:一個(gè)常見錯(cuò)誤

在ECMAScript2015引入let關(guān)鍵詞之前峦萎,閉包的一個(gè)常見的問題發(fā)生于循環(huán)中創(chuàng)建閉包,參考下面的實(shí)例

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();
循環(huán)中創(chuàng)建閉包的常見錯(cuò)誤

數(shù)組 helpText 中定義了三個(gè)有用的提示信息忆首,每一個(gè)都關(guān)聯(lián)于對(duì)應(yīng)的文檔中的輸入域的 ID爱榔。通過循環(huán)這三項(xiàng)定義,依次為每一個(gè)輸入域添加了一個(gè) onfocus 事件處理函數(shù)雄卷,以便顯示幫助信息

運(yùn)行這段代碼后搓蚪,您會(huì)發(fā)現(xiàn)它沒有達(dá)到想要的效果。無論焦點(diǎn)在哪個(gè)輸入域上丁鹉,顯示的都是關(guān)于年齡的消息妒潭。

該問題的原因在于賦給focus是閉包(setupHelp)中的匿名函數(shù)而不是閉包對(duì)象;在閉包(setupHelp)中一共創(chuàng)建了三個(gè)匿名函數(shù)揣钦,但是他們都共享與用一個(gè)環(huán)境(item)雳灾。onfus的回調(diào)被執(zhí)行時(shí),循環(huán)早已經(jīng)完成,且此時(shí)item變量(由所有三個(gè)閉包所共享)已經(jīng)指向了helpText列表的最后一項(xiàng)

解決方案一

解決這個(gè)問題的一種方案是使onfocus指向一個(gè)新的閉包對(duì)象冯凹。

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
//相當(dāng)于var help = help
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();

這段代碼可以如我們所期望的那樣工作谎亩。所有的回調(diào)不再共享同一個(gè)環(huán)江,makeHelpCallback函數(shù)為每一個(gè)回調(diào)創(chuàng)建一個(gè)新的環(huán)境宇姚、在這些環(huán)境中匈庭,help指向helpText數(shù)組中對(duì)應(yīng)的字符串

解決方案二 使用匿名閉包

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    (function() {
       var item = helpText[i];
       document.getElementById(item.id).onfocus = function() {
         showHelp(item.help);
       }
    })(); // Immediate event listener attachment with the current value of item (preserved until iteration).
  }
}

setupHelp();

解決方案三(推薦)避免使用過多的閉包,可以用let關(guān)鍵詞

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

用let而不用var,使得每個(gè)閉包綁定塊內(nèi)變量浑劳,不需要額外的閉包

性能考量

如果不是因?yàn)槟承┨厥馊蝿?wù)而需要閉包阱持,在沒有必要的請(qǐng)款項(xiàng)給I啊,在其他函數(shù)中創(chuàng)建函數(shù)是不明智的魔熏,因?yàn)殚]包對(duì)腳本性能具有負(fù)面影響衷咽,影響處理速度和內(nèi)存消耗

例如鸽扁,在創(chuàng)建新的對(duì)象或者類時(shí),方法通常應(yīng)該關(guān)聯(lián)于對(duì)象的原型镶骗,而不是定義到對(duì)象的構(gòu)造器中桶现。原因是這將導(dǎo)致每次構(gòu)造器被調(diào)用,方法都會(huì)被重新賦值一次(也就是說鼎姊,為每一個(gè)對(duì)象的創(chuàng)建)骡和。

考慮以下雖然不切實(shí)際但卻說明問題的示例:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

上面的代碼并未利用到閉包的益處,因此此蜈,應(yīng)該修改為如下常規(guī)形式

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末即横,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子裆赵,更是在濱河造成了極大的恐慌,老刑警劉巖跺嗽,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件战授,死亡現(xiàn)場離奇詭異,居然都是意外死亡桨嫁,警方通過查閱死者的電腦和手機(jī)植兰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來璃吧,“玉大人楣导,你說我怎么就攤上這事⌒蟀ぃ” “怎么了筒繁?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長巴元。 經(jīng)常有香客問我毡咏,道長,這世上最難降的妖魔是什么逮刨? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任呕缭,我火速辦了婚禮,結(jié)果婚禮上修己,老公的妹妹穿的比我還像新娘恢总。我一直安慰自己,他們只是感情好睬愤,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布片仿。 她就那樣靜靜地躺著,像睡著了一般戴涝。 火紅的嫁衣襯著肌膚如雪滋戳。 梳的紋絲不亂的頭發(fā)上钻蔑,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音奸鸯,去河邊找鬼咪笑。 笑死,一個(gè)胖子當(dāng)著我的面吹牛娄涩,可吹牛的內(nèi)容都是我干的窗怒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼蓄拣,長吁一口氣:“原來是場噩夢啊……” “哼扬虚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起球恤,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤辜昵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后咽斧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體堪置,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年张惹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了舀锨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡宛逗,死狀恐怖坎匿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情雷激,我是刑警寧澤替蔬,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站侥锦,受9級(jí)特大地震影響进栽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜恭垦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一快毛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧番挺,春花似錦唠帝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至粪摘,卻和暖如春瀑晒,著一層夾襖步出監(jiān)牢的瞬間绍坝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國打工苔悦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留轩褐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓玖详,卻偏偏與公主長得像把介,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蟋座,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容