前端進擊的巨人(三):從作用域走進閉包

進擊的巨人第三篇腿短,本篇就作用域屏箍、作用域鏈、閉包等知識點答姥,一一擊破铣除。

前端進擊的巨人(三):從作用域走進閉包

作用域

作用域:負責收集并維護由所有聲明的標識符(變量)組成的一系列查詢谚咬,并實施一套非常嚴格的規(guī)則和敬,確定當前執(zhí)行的代碼對這些標識符(變量)的訪問權限

——《你不知道的JavaScript上卷》

作用域有點像圈地盤讹开,大家劃好區(qū)域,然后各自經營管理,井水不犯河水资锰。

var globaValue = '我是全局作用域';
function foo() {
    var fooValue = '我是foo作用域';
    function bar() {
        var barValue = '我是bar作用域';
    }
}

function other() {
    var otherValue = '我是other作用域';
}
作用域

作用域的變量聲明

不同作用域下命名相同的變量不會發(fā)生沖突,"就近原則"選取文狱。

var name = '任何名字';
function getName() {
    var name = '以樂之名';
    console.log(name);    // '以樂之名'
}
console.log(name);        // '任何名字'

作用域的類型

執(zhí)行上下文環(huán)境有:全局盗棵、函數、eval尚辑。那么作用域也有三種辑鲤,ES6新增了塊級作用域。

  1. 全局作用域
  2. 函數作用域
  3. eval作用域(不推薦使用eval杠茬,暫時忽略)
  4. 塊級作用域(ES6新增)

全局作用域

JavaScript中全局環(huán)境只有一個月褥,對應的全局作用域也只有一個。沒有用var/let/const聲明的變量默認都會成為全局變量瓢喉。

function foo() {
    a = 10;
};
foo();
console.log(a);    // 10 變全局變量(意外由此發(fā)生)

函數作用域

ES6之前宁赤,想要實現局部作用域的方式,都是是通過在函數中聲明變量來實現的栓票,所以也稱函數作用域决左,支持嵌套多個。

var a = 20;
function foo() {
    var a = 10;
    console.log(a);    // 10;
}
foo();

函數中聲明變量時走贪,建議在函數起始部分聲明所有變量佛猛,方便查看,切記要用var/let/const聲明坠狡,防止手抖將局部變量變成成全局變量挚躯。

function getClient() {
    var name;
    var phone;
    var sex;
}

塊級作用域

我們先來理解什么是塊?所謂塊擦秽,其實就是被大括號{}包裹的代碼部分码荔。

if (true) {
    // 這里就是塊了漩勤,也可稱代碼塊
}

ES6前沒有塊級作用域的概念,所以{}中并沒有自己的作用域缩搅。如果我們想在ES5的環(huán)境下構建塊級作用域越败,一般都是是通過立即執(zhí)行函數來實現的。

var name = '任何名字';
(function(window) {
    var name = '以樂之名';
    console.log(name);    // '以樂之名'
}(window));
console.log(name);        // '任何名字'

ES5借助函數作用域來實現塊級作用域的方式硼瓣,會讓我們的代碼充斥大量的立即執(zhí)行函數(IIFE)究飞,不便于代碼的閱讀。好的代碼的就跟好的文章一樣堂鲤,讓閱讀的人讀來舒暢明了亿傅。

為此,ES6新增塊級作用域的概念瘟栖,使用let/const聲明變量的方式葵擎,即可將其作用域指定在代碼塊中,跟函數作用域一樣支持嵌套半哟。

let i = 0;
for (let i = 0; i < 10; i++){
    console.log(i);
}
i;    // 0

let/const不允許變量提升酬滤,必須"先聲明再使用"。這種限制寓涨,稱為"暫時性死區(qū)"盯串。這也能讓我們在代碼編寫階段變得更加規(guī)范化,執(zhí)行跟書寫順序保持一致戒良。

作用域鏈(變量查詢規(guī)則)

變量被作用域所管理体捏,那么變量在作用域中的查找規(guī)則,就是所謂的作用域鏈糯崎。

作用域鏈的用途几缭,是保證對執(zhí)行環(huán)境有權訪問的所有變量和函數的有序訪問

——《JavaScript高級程序涉及》

"在當前執(zhí)行環(huán)境開始查找使用到的變量,如果找到拇颅,則返回其值奏司。如果找不到,會逐層往上級(父作用域)查找樟插,直到全局作用域"韵洋。

var money = 100;
function foo() {
    function bar() {
        console.log(money);
    }
    bar();
}
foo();
作用域鏈上的變量查找

自由變量

變量我們見的不少,但"自由變量"聽著是不是挺唬人的黄锤。其實對它搪缨,我們并不陌生。

"自由變量:當前執(zhí)行環(huán)境使用到鸵熟,但并未在當前執(zhí)行環(huán)境聲明的變量(函數參數arguments排除)"

函數調用時副编,進入執(zhí)行上下文創(chuàng)建階段,會對argument進行隱式的變量聲明流强。

var outer = '我是外面變量';
function foo() {
    var inner = '我是里面變量痹届,不是自由變量';
    console.log(outer);   
    // 這里用到了outer呻待,但outer并不在函數foo中聲明,所以outer就是foo中的自由變量
}

"自由變量的作用域由詞法環(huán)境決定队腐,也就是它的作用域在代碼書寫階段就已經確定了蚕捉,而不是在代碼編譯執(zhí)行階段確定。"

"自由變量的值是在代碼執(zhí)行時確定的柴淘,變量變量變量迫淹,值肯定要變,所以自由變量的值只有在程序運行階段才能確定为严。"

閉包

開篇第一文我們就執(zhí)行環(huán)境敛熬,執(zhí)行棧做出了詳解,有所遺忘的可再溫習第股。執(zhí)行棧是我們理解閉包原理基礎中的基礎应民。

函數調用棧過程的圖再曬出來,順便溫習下炸茧。

function foo () {
    function bar () {
        return 'I am bar';
    }
    return bar();
}
foo();
正常出入棧過程

函數調用時入棧瑞妇,調用結束出棧稿静。執(zhí)行函數時梭冠,會創(chuàng)建一個變量對象去存儲函數中的變量,方法改备,參數arguments等控漠,結束調用時,該變量對象就會被銷毀悬钳。(理想的情況下盐捷,不理想的情況就是出現"閉包"調用了)。

什么是閉包默勾?

閉包是指有權訪問另外一個函數作用域的變量的函數碉渡。

——《JavaScript高級程序設計》

閉包是指那些能夠訪問自由變量的函數。

——MDN

閉包的特點首先是函數母剥,其次是它可以訪問到父級作用域的變量對象滞诺,即使父級函數完成調用后"理應出棧銷毀"

判定閉包出現

  1. 函數作為參數傳遞
  2. 函數作為返回值傳遞
function foo() {
    var fooVal = '2019';
    var bar = function() {
        console.log(fooVal);    // bar中使用到了自由變量fooVal
    }
    return bar;                 // 函數作為參數返回
}

var getValue = foo();
getValue();                     // 2019

對函數中誰是閉包环疼,各文檔解釋不一习霹。在此我們遵照Chrome的方式,暫且稱foo是閉包炫隶。

因為作用域和作用域鏈規(guī)則的限定淋叶,子環(huán)境的自由變量只能逐層向上到父環(huán)境查找。

但是通過閉包伪阶,我們在外部環(huán)境也可以獲取到變量fooVal煞檩,雖然foo()函數執(zhí)行完成了处嫌,但它并沒從函數調用棧中銷毀,其變量對象存儲仍然能被訪問到斟湃。

實際執(zhí)行過程請看圖:


存在閉包的出入棧過程

把上述代碼改以下锰霜,接著看:

function foo() {
 var fooVal = '2019';
 var bar = function() {
 console.log(fooVal);     // bar中使用到了自由變量fooVal
 }
 return bar;              // 函數作為參數返回
}
var getValue = foo();
var fooVal = '2018';      // 這里的fooVal是全局作用域的變量
getValue();               // 2019

答案與結果不符的小伙伴要回頭理解下自由變量了。"自由變量的作用域在代碼書寫時(函數創(chuàng)建時)就確定了"桐早,所以函數中getValue()使用的fooValfoo的作用域下癣缅,而不是在全局作用域下。

答對的小伙伴們再來一道題哄酝,加深你的記憶

function fn() {
    var max = 10;
    function bar(x) {
        if (x > max) {    
            console.log(x)
        }
    }
    return bar;
}
var f1 = fn();
var max = 100;

f1(20);                 // 輸出20

題目解析:max作為函數bar中的自由變量友存,它的作用域在函數bar創(chuàng)建的時候就確定了,就是函數fn中的max陶衅,所以它的作用域鏈查找到fn中已經結束并返回了屡立,不會再向上找到全局作用域。

注意:棧中存儲的不只是閉包中使用到的自由變量搀军,而是父級函數的整個變量對象(父級函數作用域中聲明的方法膨俐,變量,參數等)

閉包的應用場景

上文中已經闡述了閉包的特點罩句,就是能夠讓我們跨作用域取值(不局限于父子作用域)焚刺。列舉兩個實際開放中常用的栗子:

  1. 封裝回調保存作用域
for(var i = 1; i < 5; i++) {
    setTimeout((function(i){
       return function() {
           console.log(i);        
       } 
    })(i), i * 1000)
}
// 原理:通過自執(zhí)行函數傳參i,然后返回一個函數(閉包)中使用i门烂,使父函數的變量對象一直存在
  1. 私有變量和方法實現模塊化
var makePeople = function () {
    var _name = '以樂之名';
    return {
        getName: function () {
            console.log(_name);
        },
        setName: function (name) {
            if (name != 'Hello world') {
                _name = name;
            }
        }
    }
}

var me = makePeople();
me.getName();                   // '以樂之名'
me.setName('KenTsang');         
me.getName();                   // 'KenTsang'

// 原理:私有變量_name沒有對外訪問權限乳愉,但通過閉包使其一直保留在內存中,可以被外部調用

閉包的應用場景還有很多屯远,具體實際情況還需具體分析蔓姚。

閉包造成的內存泄露

閉包的使用,破壞了函數的出棧過程慨丐。解釋執(zhí)行棧的時候坡脐,講到同個函數即使調用自身,創(chuàng)建的變量對象也并非同一個房揭,其內存存儲是各自獨立的备闲。

棧中只入不出,函數的變量對象沒有被有效回收崩溪,就會造成瀏覽器內存占用逐步增加浅役,內存占用過高的情況下,就會導致頁面卡頓伶唯,甚至瀏覽器崩潰觉既。這就是我們常說的閉包造成的"內存泄露"

所以,一名合格的前端瞪讼,除了會用閉包钧椰,還要正確的解除閉包引用。
垃圾回收機制講解時符欠,通過設置變量值為null時可已解除變量的引用嫡霞,以便下一次垃圾回收銷毀它。

function foo() {
 var fooVal = '2019';
 var bar = function() {
 console.log(fooVal);     
 }
 return bar;              
}
var getValue = foo();
var fooVal = '2018';     
getValue();
getValue = null;         // 解除引用希柿,下一次垃圾回收就會回收了

寫在結尾

閉包算是前端初學者的一個難點诊沪,能解釋清楚并不容易,涉及到作用域曾撤,執(zhí)行上下文環(huán)境端姚、變量對象等等。

零散知識的內聚匯總挤悉,正是是系列更文的初衷所在渐裸。

知識不是小段子,聽完笑過就忘装悲,唯有形成體系昏鹃,達成閉環(huán),才能深植入記憶中诀诊。


參考文檔:

本文首發(fā)Github洞渤,期待Star!
https://github.com/ZengLingYong/blog

作者:以樂之名
本文原創(chuàng)畏梆,有不當的地方歡迎指出您宪。轉載請指明出處奈懒。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末奠涌,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子磷杏,更是在濱河造成了極大的恐慌溜畅,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件极祸,死亡現場離奇詭異慈格,居然都是意外死亡,警方通過查閱死者的電腦和手機遥金,發(fā)現死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門浴捆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人稿械,你說我怎么就攤上這事选泻。” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵页眯,是天一觀的道長梯捕。 經常有香客問我,道長窝撵,這世上最難降的妖魔是什么傀顾? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮碌奉,結果婚禮上短曾,老公的妹妹穿的比我還像新娘。我一直安慰自己赐劣,他們只是感情好错英,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著隆豹,像睡著了一般椭岩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上璃赡,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天判哥,我揣著相機與錄音,去河邊找鬼碉考。 笑死塌计,一個胖子當著我的面吹牛,可吹牛的內容都是我干的侯谁。 我是一名探鬼主播锌仅,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼墙贱!你這毒婦竟也來了热芹?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤惨撇,失蹤者是張志新(化名)和其女友劉穎伊脓,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體魁衙,經...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡报腔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了剖淀。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纯蛾。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖纵隔,靈堂內的尸體忽然破棺而出翻诉,到底是詐尸還是另有隱情帆卓,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布米丘,位于F島的核電站剑令,受9級特大地震影響,放射性物質發(fā)生泄漏拄查。R本人自食惡果不足惜吁津,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望堕扶。 院中可真熱鬧碍脏,春花似錦、人聲如沸稍算。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽糊探。三九已至钾埂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間科平,已是汗流浹背褥紫。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瞪慧,地道東北人髓考。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像弃酌,于是被迫代替她去往敵國和親氨菇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內容