js 閉包

一贤重、參考一個例子讓你讀懂什么是JS閉包

需要在公司頁面上顯示一個瀏覽時間菜拓,從打開頁面的瞬間開始計時遗菠,每過一秒鐘加一

1.全局變量解決方案
//秒數(shù)
let second = 0;

//累加器
function counter(){
   second += 1;
   return second;
}

const recordSecond = setInterval(function(){
   //到達10秒后停止
   if(second === 10){
      clearInterval(recordSecond);
      console.log('計時結(jié)束纹蝴!');
      return;
   }
   //調(diào)用累加器盐杂,輸出當前秒數(shù)
   console.log(`${counter()}秒`);
},1000);

在chrome控制臺執(zhí)行一下結(jié)果,可以看到我們已經(jīng)實現(xiàn)了所需要的功能批什,每次都更新一個全局變量农曲。但是,所有編程語言中有一條不成文的鐵律:盡可能的少定義全局變量渊季。

  • 全局變量不好控制朋蔫,可以在任何地方進行讀寫罚渐,這意味著可能會被不想干的程序改寫却汉。
  • 全局變量占用內(nèi)存的生命周期長,一般局部變量荷并,定義在函數(shù)中合砂,在函數(shù)調(diào)用完成后,與之對應(yīng)的執(zhí)行環(huán)境會退出執(zhí)行棧源织,回收機制會每隔一段時間進行一次回收翩伪。而全局變量因為隨時可以被任何程序在任何地方讀寫微猖,會導(dǎo)致全局變量一般在全局執(zhí)行環(huán)境被銷毀時才會釋放。
2.優(yōu)化一下

首先缘屹,將判斷停止定時任務(wù)的條件放在counter函數(shù)中

//秒數(shù)
let second = 0;

//累加器
function counter(){
   //到達10秒后停止
   if(second === 10){
      clearInterval(recordSecond);
      console.log('計時結(jié)束凛剥!');
      return;
   }
   second += 1;
   console.log(`${second}秒`);
}

const recordSecond = setInterval(function(){
   //調(diào)用累加器,輸出當前秒數(shù)
   counter();
},1000);

接下來轻姿,我們將second定義為局部變量犁珠,因為setInterval的回調(diào)函數(shù)每隔一秒就執(zhí)行一次,second聲明在回調(diào)函數(shù)中互亮,每次回調(diào)函數(shù)被調(diào)用的同時second就會被初始化犁享,那么就實現(xiàn)不了累加的效果:

//累加器
function counter(){
   //秒數(shù)
   let second = 0;
   
   //到達10秒后停止
   if(second === 10){
      clearInterval(recordSecond);
      console.log('計時結(jié)束!');
      return;
   }
   second += 1;
   console.log(`${second}秒`);
}

const recordSecond = setInterval(function(){
   //調(diào)用累加器豹休,輸出當前秒數(shù)
   counter();
},1000);

想必大家已經(jīng)發(fā)現(xiàn)問題了炊昆,因為counter的調(diào)用是在回調(diào)函數(shù)中的,那么單純將second定義在counter中威根,也避免不了被初始化的操作凤巨。

//累加器
function counter(){
   //秒數(shù)
   let second = 0;

   function doCounter(){
      //到達10秒后停止
       if(second === 10){
          clearInterval(recordSecond);
          console.log('計時結(jié)束!');
          return;
       }
       second += 1;
       console.log(`${second}秒`);
   }

}

const recordSecond = setInterval(function(){
   //調(diào)用累加器医窿,輸出當前秒數(shù)
   counter();
},1000);

現(xiàn)在磅甩,從代碼結(jié)構(gòu)上,我們將counter分成了兩部分姥卢,因為doCounter是counter的內(nèi)部函數(shù)卷要,有權(quán)訪問外部函數(shù)作用域中的變量second。但是独榴,還有一個問題僧叉,我們沒有將doCounter返回,那么就算counter被調(diào)用棺榔,也只是執(zhí)行了對其內(nèi)部second和doCounter()的聲明操作瓶堕。很簡單,我們把doCounter前面添加return即可:

//累加器
function counter(){
   //秒數(shù)
   let second = 0;

   function doCounter(){
      //到達10秒后停止
       if(second === 10){
          clearInterval(recordSecond);
          console.log('計時結(jié)束症歇!');
          return;
       }
       second += 1;
       console.log(`${second}秒`);
   }
   return doCounter;
}

const recordSecond = setInterval(function(){
   //調(diào)用累加器郎笆,輸出當前秒數(shù)
   counter();
},1000);

但是執(zhí)行這段代碼,卻發(fā)現(xiàn)控制臺沒有輸出任何信息忘晤。那么問題在哪呢宛蚓?counter()函數(shù)確實執(zhí)行了,但是它只是拿到了返回的doCounter函數(shù)设塔,但并未調(diào)用凄吏。為了提高可讀性,修改如下:

//累加器
function counter(){
   //秒數(shù)
   let second = 0;

   function doCounter(){
      //到達10秒后停止
       if(second === 10){
          clearInterval(recordSecond);
          console.log('計時結(jié)束!');
          return;
       }
       second += 1;
       console.log(`${second}秒`);
   }

   return doCounter;
}

const doCounterFn = counter();

const recordSecond = setInterval(function(){
   //調(diào)用累加器
   doCounterFn();
},1000);

當我們通過doCounterFn 間接調(diào)用doCounter時痕钢,雖然doCounterFn 的作用域鏈上并不存在變量second图柏,但doCounter被執(zhí)行時依舊能訪問它的作用域鏈上的變量,也就是它聲明時所在的作用域內(nèi)的任何變量任连,這就是作用域延長的典型例子蚤吹。

通過counter 和 doCounter兩個函數(shù)嵌套,形成作用域的嵌套随抠,被嵌套函數(shù)需要對所在作用域進行訪問距辆,再將被嵌套的函數(shù)在另一個作用域中調(diào)用,這一整個過程就是我們所說的閉包暮刃。

3.總結(jié)
  • 什么時間需要使用閉包跨算?當我們需要重復(fù)使用一個對象,但又想保護這個對象不被其他代碼污染
  • 閉包的作用椭懊?使得一個外部函數(shù)有權(quán)訪問一個內(nèi)部函數(shù)作用域诸蚕。
  • 閉包的形成必備條件?需要訪問作用域氧猬;函數(shù)嵌套(物理條件)背犯;被嵌套函數(shù)在另一個外部作用域中被調(diào)用
  • 閉包的缺點?比起普通函數(shù)閉包對內(nèi)存的占用更多盅抚,建議使用完畢后漠魏,手動標空fn=null
二、參考「每日一題」JS 中的閉包是什么妄均?
function foo(){
  var local = 1
  function bar(){
    local++
    return local
  }
  return bar
}

var func = foo()
func()
1.為什么要函數(shù)套函數(shù)呢柱锹?

是因為需要局部變量,所以才把 local 放在一個函數(shù)里丰包,如果不把 local 放在一個函數(shù)里禁熏,local 就是一個全局變量了,達不到使用閉包的目的——隱藏變量(等會會講)邑彪。這也是為什么我上面要說「運行在一個立即執(zhí)行函數(shù)中」瞧毙。

有些人看到「閉包」這個名字,就一定覺得要用什么包起來才行寄症。其實這是翻譯問題宙彪,閉包的原文是 Closure,跟「包」沒有任何關(guān)系有巧。所以函數(shù)套函數(shù)只是為了造出一個局部變量释漆,跟閉包無關(guān)。

2.為什么要 return bar 呢剪决?

因為如果不 return灵汪,你就無法使用這個閉包。把 return bar 改成 window.bar = bar 也是一樣的柑潦,只要讓外面可以訪問到這個 bar 函數(shù)就行了享言。所以 return bar 只是為了 bar 能被使用,也跟閉包無關(guān)渗鬼。

三览露、參考破解前端面試(80% 應(yīng)聘者不及格系列):從 閉包說起
for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
}

console.log(new Date, i);
  • A. 20% 的人會快速掃描代碼,然后給出結(jié)果:0,1,2,3,4,5譬胎;
  • B. 30% 的人會拿著代碼逐行看差牛,然后給出結(jié)果:5,0,1,2,3,4;
  • C. 50% 的人會拿著代碼仔細琢磨堰乔,然后給出結(jié)果:5,5,5,5,5,5偏化;

只要你對 JS 中同步和異步代碼的區(qū)別、變量作用域镐侯、閉包等概念有正確的理解侦讨,就知道正確答案是 C,代碼的實際輸出是:

2017-03-18T00:43:45.873Z 5
2017-03-18T00:43:46.866Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5

接下來我會追問:如果我們約定苟翻,用箭頭表示其前后的兩次輸出之間有 1 秒的時間間隔韵卤,而逗號表示其前后的兩次輸出之間的時間間隔可以忽略,代碼實際運行的結(jié)果該如何描述崇猫?會有下面兩種答案:

  • A. 60% 的人會描述為:5 -> 5 -> 5 -> 5 -> 5沈条,即每個 5 之間都有 1 秒的時間間隔;
  • B. 40% 的人會描述為:5 -> 5,5,5,5,5诅炉,即第 1 個 5 直接輸出蜡歹,1 秒之后,輸出 5 個 5涕烧;

這就要求候選人對 JS 中的定時器工作機制非常熟悉季稳,循環(huán)執(zhí)行過程中,幾乎同時設(shè)置了 5 個定時器澈魄,一般情況下景鼠,這些定時器都會在 1 秒之后觸發(fā),而循環(huán)完的輸出是立即執(zhí)行的痹扇,顯而易見铛漓,正確的描述是 B。

1.追問 1:閉包

如果這道題僅僅是考察候選人對 JS 異步代碼鲫构、變量作用域的理解浓恶,局限性未免太大,接下來我會追問结笨,如果期望代碼的輸出變成:5 -> 0,1,2,3,4包晰,該怎么改造代碼湿镀?熟悉閉包的同學(xué)很快能給出下面的解決辦法:

for (var i = 0; i < 5; i++) {
    (function(j) {  // j = i
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000);
    })(i);
}

console.log(new Date, i);

巧妙的利用 IIFE(Immediately Invoked Function Expression:聲明即執(zhí)行的函數(shù)表達式)來解決閉包造成的問題,確實是不錯的思路伐憾,但是初學(xué)者可能并不覺得這樣的代碼很好懂勉痴,至少筆者初入門的時候這里琢磨了一會兒才真正理解。

有沒有更符合直覺的做法树肃?答案是有蒸矛,我們只需要對循環(huán)體稍做手腳,讓負責輸出的那段代碼能拿到每次循環(huán)的 i 值即可胸嘴。該怎么做呢雏掠?利用 JS 中基本類型(Primitive Type)的參數(shù)傳遞是按值傳遞(Pass by Value)的特征,不難改造出下面的代碼:

var output = function (i) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
};

for (var i = 0; i < 5; i++) {
    output(i);  // 這里傳過去的 i 值被復(fù)制了
}

console.log(new Date, i);

能給出上述 2 種解決方案的候選人可以認為對 JS 基礎(chǔ)的理解和運用是不錯的劣像,可以各加 10 分乡话。當然實際面試中還有候選人給出如下的代碼:

for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
}

console.log(new Date, i);

細心的同學(xué)會發(fā)現(xiàn),這里只有個非常細微的變動耳奕,即使用 ES6 塊級作用域(Block Scope)中的 let 替代了 var蚊伞,但是代碼在實際運行時會報錯,因為最后那個輸出使用的 i 在其所在的作用域中并不存在吮铭,i 只存在于循環(huán)內(nèi)部时迫。能想到 ES6 特性的同學(xué)雖然沒有答對,但是展示了自己對 ES6 的了解谓晌,可以加 5 分掠拳,繼續(xù)進行下面的追問。

2.追問 2:ES6

有經(jīng)驗的前端同學(xué)讀到這里可能有些不耐煩了纸肉,扯了這么多溺欧,都是他知道的內(nèi)容,先別著急柏肪,挑戰(zhàn)的難度會繼續(xù)增加姐刁。

接著上文繼續(xù)追問:如果期望代碼的輸出變成 0 -> 1 -> 2 -> 3 -> 4 -> 5,并且要求原有的代碼塊中的循環(huán)和兩處 console.log 不變烦味,該怎么改造代碼聂使?新的需求可以精確的描述為:代碼執(zhí)行時,立即輸出 0谬俄,之后每隔 1 秒依次輸出 1,2,3,4柏靶,循環(huán)結(jié)束后在大概第 5 秒的時候輸出 5(這里使用大概,是為了避免鉆牛角尖的同學(xué)陷進去溃论,因為 JS 中的定時器觸發(fā)時機有可能是不確定的屎蜓,具體可參見 How Javascript Timers Work)。

順著下來钥勋,不難給出基于 Promise 的解決方案(既然 Promise 是 ES6 中的新特性炬转,我們的新代碼使用 ES6 編寫是不是會更好辆苔?如果你這么寫了,大概率會讓面試官心生好感):

const tasks = [];
for (var i = 0; i < 5; i++) {   // 這里 i 的聲明不能改成 let扼劈,如果要改該怎么做驻啤?
    ((j) => {
        tasks.push(new Promise((resolve) => {
            setTimeout(() => {
                console.log(new Date, j);
                resolve();  // 這里一定要 resolve,否則代碼不會按預(yù)期 work
            }, 1000 * j);   // 定時器的超時時間逐步增加
        }));
    })(i);
}

Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i);
    }, 1000);   // 注意這里只需要把超時設(shè)置為 1 秒
});

相比而言测僵,筆者更傾向于下面這樣看起來更簡潔的代碼,要知道編程風格也是很多面試官重點考察的點谢翎,代碼閱讀時的顆粒度更小捍靠,模塊化更好,無疑會是加分點森逮。

const tasks = []; // 這里存放異步操作的 Promise
const output = (i) => new Promise((resolve) => {
    setTimeout(() => {
        console.log(new Date, i);
        resolve();
    }, 1000 * i);
});

// 生成全部的異步操作
for (var i = 0; i < 5; i++) {
    tasks.push(output(i));
}

// 異步操作完成之后榨婆,輸出最后的 i
Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i);
    }, 1000);
});

讀到這里的同學(xué),恭喜你褒侧,你下次面試遇到類似的問題良风,至少能拿到 80 分。

3.追問 3:ES7

既然你都看到這里了闷供,那就再堅持 2 分鐘烟央,接下來的內(nèi)容會讓你明白你的堅持是值得的。

多數(shù)面試官在決定聘用某個候選人之前還需要考察另外一項重要能力歪脏,即技術(shù)自驅(qū)力疑俭,直白的說就是候選人像有內(nèi)部的馬達在驅(qū)動他,用漂亮的方式解決工程領(lǐng)域的問題婿失,不斷的跟隨業(yè)務(wù)和技術(shù)變得越來越牛逼钞艇,究竟什么是牛逼?建議閱讀程序人生的這篇剖析豪硅。

回到正題哩照,既然 Promise 已經(jīng)被拿下,如何使用 ES7 中的 async await 特性來讓這段代碼變的更簡潔懒浮?你是否能夠根據(jù)自己目前掌握的知識給出答案飘弧?請在這里暫停 1 分鐘,思考下砚著。

下面是筆者給出的參考代碼:

// 模擬其他語言中的 sleep眯牧,實際上可以是任何異步操作
const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});

(async () => {  // 聲明即執(zhí)行的 async 函數(shù)表達式
    for (var i = 0; i < 5; i++) {
        await sleep(1000);
        console.log(new Date, i);
    }

    await sleep(1000);
    console.log(new Date, i);
})();
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赖草,隨后出現(xiàn)的幾起案子学少,更是在濱河造成了極大的恐慌,老刑警劉巖秧骑,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件版确,死亡現(xiàn)場離奇詭異扣囊,居然都是意外死亡,警方通過查閱死者的電腦和手機绒疗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進店門侵歇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吓蘑,你說我怎么就攤上這事惕虑。” “怎么了磨镶?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵溃蔫,是天一觀的道長。 經(jīng)常有香客問我琳猫,道長伟叛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任脐嫂,我火速辦了婚禮统刮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘账千。我一直安慰自己侥蒙,他們只是感情好,可當我...
    茶點故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布匀奏。 她就那樣靜靜地躺著辉哥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪攒射。 梳的紋絲不亂的頭發(fā)上醋旦,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天,我揣著相機與錄音会放,去河邊找鬼饲齐。 笑死,一個胖子當著我的面吹牛咧最,可吹牛的內(nèi)容都是我干的捂人。 我是一名探鬼主播,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼矢沿,長吁一口氣:“原來是場噩夢啊……” “哼滥搭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起捣鲸,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤瑟匆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后栽惶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體愁溜,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡疾嗅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了冕象。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片代承。...
    茶點故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖渐扮,靈堂內(nèi)的尸體忽然破棺而出论悴,到底是詐尸還是另有隱情,我是刑警寧澤墓律,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布膀估,位于F島的核電站,受9級特大地震影響只锻,放射性物質(zhì)發(fā)生泄漏玖像。R本人自食惡果不足惜紫谷,卻給世界環(huán)境...
    茶點故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一齐饮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧笤昨,春花似錦祖驱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至崇裁,卻和暖如春匕坯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拔稳。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工葛峻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人巴比。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓术奖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親轻绞。 傳聞我的和親對象是個殘疾皇子采记,可洞房花燭夜當晚...
    茶點故事閱讀 43,494評論 2 348

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

  • 1,javascript 基礎(chǔ)知識 Array對象 Array對象屬性 Arrray對象方法 Date對象 Dat...
    Yuann閱讀 896評論 0 1
  • 普通創(chuàng)建對象和字面量創(chuàng)建對象不足之處:雖然 Object 構(gòu)造函數(shù)或?qū)ο笞置媪慷伎梢杂脕韯?chuàng)建單個對象,但這些方式有...
    believedream閱讀 2,362評論 2 18
  • 繼承 一政勃、混入式繼承 二唧龄、原型繼承 利用原型中的成員可以被和其相關(guān)的對象共享這一特性,可以實現(xiàn)繼承奸远,這種實現(xiàn)繼承的...
    magic_pill閱讀 1,054評論 0 3
  • 萬水千山走遍 撒哈拉的故事 如彗星劃過夜空 那夜的燭光七里香 文字里的情愫 在我翻開時與我的念頭相遇 只有你我知道...
    王不煩閱讀 128評論 0 1
  • 殊途同歸的結(jié)局戏挡,到底不一樣在哪里? 短短的一生晨仑,以目前這壽命率來看褐墅,長命百歲已是稀罕,普通人大概可以存在這個世界七...
    清醒的女王閱讀 358評論 6 2