JavaScript 作用域和閉包理解

作用域:

分為函數(shù)作用域氯哮,和塊級作用域脂凶;

函數(shù)作用域

函數(shù)作用域外面的無法訪問函數(shù)作用域內(nèi)部的變量和函數(shù)敲街,這樣就可以將一些變量和函數(shù)隱藏起來鼻百;

隱藏起來的好處是

  1. 形成命名空間绞旅,避免各個函數(shù)里面的變量沖突
  2. 實(shí)現(xiàn)模塊管理

內(nèi)部可以訪問外部的;


function foo(a) { var b = 2;
    // 一些代碼
    function bar() {
    // ...
    }
    // 更多的代碼 var c = 3;
}

bar(); // 失敗
console.log( a, b, c ); // 三個全都失敗

此時温艇,foo里面就是一個函數(shù)作用域因悲,可以bar里面又是一個作用域;最外面當(dāng)然就是全局作用域勺爱;

可以把函數(shù)看著一個可以單向向外訪問的圈子晃琳;

函數(shù)表達(dá)式 vs 函數(shù)聲明

這里需要重點(diǎn)區(qū)分一下:

  1. 函數(shù)聲明: function 是聲明中 的第一個詞,那么就是一個函數(shù)聲明邻寿;
  2. 函數(shù)表達(dá)式:除此之外就是函數(shù)表達(dá)式蝎土;
  3. 函數(shù)表達(dá)式可以是匿名的,函數(shù)聲明不可以绣否;

函數(shù)聲明和函數(shù)表達(dá)式之間最重要的區(qū)別是它們的名稱標(biāo)識符將會綁定在何處誊涯;

例子

var a = 2;
(function foo(){ // <-- 添加這一行 
    var a = 3;
    console.log( a ); // 3
})(); // <-- 以及這一行 
console.log( a ); // 2
  1. 這里的(function foo(){ .. })是一個函數(shù)表達(dá)式;而不是一 個標(biāo)準(zhǔn)的函數(shù)聲明蒜撮;
    所以量瓜,foo 被綁定在函數(shù)表達(dá)式自身的函數(shù)中而不是所在作用域中语淘。

  2. 換句話說圆裕,(function foo(){ .. })作為函數(shù)表達(dá)式意味著foo只能在..所代表的位置中被訪問蜓竹,外部作用域則不行。foo 變量名被隱藏在自身中意味著不會非必要地污染外部作 用域苹支。

  3. 立即執(zhí)行函數(shù)表達(dá)式:由于foo被包含在一對( )括號內(nèi)部砾隅,因此成為了一個表達(dá)式;通過在末尾加上另外一個 ( ) 可以立即執(zhí)行這個函數(shù)债蜜;即晴埂, (function foo(){ .. })()究反。第一個 ( ) 將函數(shù)變成表 達(dá)式,第二個 ( ) 執(zhí)行了這個函數(shù)儒洛。

  4. 還可以穿參數(shù)

var a = 2;
(function IIFE( global ) {
    var a = 3;
    console.log( a ); // 3 
    console.log( global.a ); // 2
})( window );
console.log( a ); // 2

這樣就可以訪問外面的a了精耐,因?yàn)樵L問變量a的時候就近原則,就得到了3琅锻;

塊級作用域

1. { }

var 定義的變量和函數(shù)卦停,和在當(dāng)前塊級所處作用域定義沒有什么區(qū)別;
let恼蓬,const 在塊級作用域外面就訪問不到惊完;

簡單的說{ }這個就形成了一個塊級作用域;

例子

{
    var a = 44;
    let b = 22;
    const c = 33
}
a // 44;
b // Uncaught ReferenceError: b is not defined;找不到引用滚秩,報錯专执;
c // Uncaught ReferenceError: c is not defined;找不到引用淮捆,報錯郁油;

2. with

用 with 從對象中創(chuàng)建出的作用域僅在 with 聲明中而非外 部作用域中有效。

3. try/catch

JavaScript 的 ES3 規(guī)范中規(guī)定 try/catch 的 catch 分句會創(chuàng)建一個塊作
用域攀痊,其中聲明的變量僅在 catch 內(nèi)部有效桐腌。

閉包

無論論通過何種手段將內(nèi)部函數(shù)傳遞到所在的作用域以外,它都會持有對原始定義作用域的引用苟径,無論在何處執(zhí)行這個函數(shù)都會使用閉包案站。

個人所理解的閉包就是一個作用域,及作用域內(nèi)的變量和函數(shù)的緩存棘街;不會被釋放了蟆盐,以供在今后訪問;不得被垃圾回收掉

不知道大家是否還記得JavaScript的垃圾回收機(jī)制遭殉;

垃圾收集:就是執(zhí)行完后石挂,對沒有引用的變量進(jìn)行釋放;常見手動釋放就是設(shè)置成null险污;

例子1

function foo() { 
    var a = 2;
    function bar() { 
        console.log( a );
    }
    return bar; 
}
var baz = foo();
baz(); // 2 —— 朋友痹愚,這就是閉包的效果。

分析:

  1. 函數(shù) bar() 的作用域能夠訪問 foo() 的內(nèi)部作用域蛔糯。
    bar() 顯然可以被正常執(zhí)行拯腮。并且,它能在自己定義的作用域以外的地方 執(zhí)行蚁飒。
  2. 在 foo() 執(zhí)行后动壤,通常會期待foo()的整個內(nèi)部作用域都被銷毀,因?yàn)槲覀冎酪嬗欣厥掌饔脕磲尫挪辉偈褂玫膬?nèi)存空間淮逻。由于看上去 foo() 的內(nèi)容不會再被使用琼懊,所以很自然地會考慮對其進(jìn)行回收蜒灰。
  3. 而閉包的“神奇”之處正是可以阻止這件事情的發(fā)生。事實(shí)上內(nèi)部作用域依然存在肩碟,因此沒有被回收强窖。誰在使用這個內(nèi)部作用域?原來是 bar() 本身在使用。
  4. 拜 bar() 所聲明的位置所賜削祈,它擁有涵蓋foo()內(nèi)部作用域的閉包翅溺,使得該作用域能夠一 直存活,以供 bar() 在之后任何時間進(jìn)行引用髓抑。

bar() 依然持有對該作用域的引用咙崎,而這個引用就叫作閉包。

例子2

function foo() { 
    var a = 2;
    function baz() {
        console.log( a ); // 2
    }
    bar( baz ); 
}
function bar(fn) {
    fn(); // 媽媽快看呀吨拍,這就是閉包!
}
foo(); // 2

等效如下:

var fn;
function foo() {
    var a = 2;
    function baz() { 
        console.log( a );
    }
    fn = baz; //將baz分配給全局變量 
}
function bar() {
    fn(); // 媽媽快看呀褪猛,這就是閉包!
}
foo();
bar(); // 2

按正常的作用域思考方式,bar是沒有辦法訪問foo的內(nèi)部的變量的羹饰;

  1. 但是foo可以訪問外部作用域下的bar伊滋;
  2. bar在foo內(nèi)部;
  3. 將baz傳遞給bar的內(nèi)部队秩,baz無論在哪里都依然持有對foo內(nèi)部變量的引用笑旺;

baz 和 變量a,還有foo形成了一個閉包馍资,這個作用域?qū)⒈灰婢彺嫫饋硗仓鳎籦az隨時都可以訪問;

function foo(a) { 
    var b = 2;
    // 一些代碼
    function bar() {
        // ...
    }
    // 更多的代碼 
    var c = 3;
}
foo();
bar(); // 失敗
console.log( a, b, c ); // 三個全都失敗

這種就無法訪問foo里面的變量和函數(shù)了鸟蟹,因?yàn)閒oo里面都是局部變量乌妙,外部無法直接訪問,這種里面變量再會被其他地方引用建钥,將會被引擎垃圾回收釋放掉藤韵。

形成閉包的條件

  1. 一個函數(shù)foo包含一個函數(shù)baz和一個變量a;(名字隨意)
  2. baz內(nèi)部存在對a的引用锦针;
  3. foo需要被執(zhí)行荠察;

正確例子示范

function wait(message) {
    setTimeout( function timer() {
        console.log( message );
    }, 1000 ); 
}

wait( "Hello, closure!" );

這就是個閉包;

  1. timer和message都在wait內(nèi)部
  2. timer對wait的message有引用奈搜;
  3. wait被執(zhí)行了

一般來說悉盆,只要使 用了回調(diào)函數(shù),實(shí)際上就是在使用閉包!

錯誤例子示范

for (var i=1; i<=5; i++) { 
    (function() {
        setTimeout( function timer() { 
            console.log( i );
        }, i*1000 );
    })();
}
// 打印5次6馋吗;

這樣不行焕盟!這只是一個都沒有的空作用域。不能形成閉包

修改1:

for (var i=1; i<=5; i++) { 
    (function(j) {
        setTimeout( function timer() { 
            console.log( j );
        }, j*1000 );
    })(i);//從外部傳進(jìn)來
}

修改2:

for (let i=1; i<=5; i++) { 
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}
//塊作用域和閉包聯(lián)手便可天下無敵

應(yīng)用——模塊

模塊有兩個主要特征:

  1. 為創(chuàng)建內(nèi)部作用域而調(diào)用了一個包裝函數(shù);
  2. 包裝函數(shù)的返回 值必須至少包括一個對內(nèi)部函數(shù)的引用,這樣就會創(chuàng)建涵蓋整個包裝函數(shù)內(nèi)部作用域的閉包脚翘。
MyModules.define( "bar", [], function() { 
    function hello(who) {
        return "Let me introduce: " + who;
    }
    return {
     hello: hello
    }; 
} );

MyModules.define( "foo", ["bar"], function(bar) {
    var hungry = "hippo";
    function awesome(){
        console.log( bar.hello( hungry ).toUpperCase() )
    }
    return {
        awesome: awesome
    }; 
} );

var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(
    bar.hello( "hippo" )
); // Let me introduce: hippo 
foo.awesome(); // LET ME INTRODUCE: HIPPO

"foo" 和 "bar" 模塊通過一個返回公共 API 的函數(shù)來定義的灼卢。"foo" 甚至接受 "bar" 的 示例作為依賴參數(shù),并能相應(yīng)地使用它来农。

總結(jié)

最后記仔妗:當(dāng)函數(shù)可以記住并訪問所在的作用域,即使函數(shù)是在當(dāng)前作用域之外執(zhí)行沃于,這時 就產(chǎn)生了閉包涩咖。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市繁莹,隨后出現(xiàn)的幾起案子檩互,更是在濱河造成了極大的恐慌,老刑警劉巖咨演,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件闸昨,死亡現(xiàn)場離奇詭異,居然都是意外死亡薄风,警方通過查閱死者的電腦和手機(jī)饵较,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來村刨,“玉大人告抄,你說我怎么就攤上這事撰茎∏段” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵龄糊,是天一觀的道長逆粹。 經(jīng)常有香客問我,道長炫惩,這世上最難降的妖魔是什么僻弹? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任他嚷,我火速辦了婚禮,結(jié)果婚禮上筋蓖,老公的妹妹穿的比我還像新娘。我一直安慰自己粘咖,他們只是感情好蚣抗,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布翰铡。 她就那樣靜靜地躺著钝域,像睡著了一般锭魔。 火紅的嫁衣襯著肌膚如雪例证。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天迷捧,我揣著相機(jī)與錄音战虏,去河邊找鬼。 笑死党涕,一個胖子當(dāng)著我的面吹牛烦感,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播膛堤,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼手趣,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了肥荔?” 一聲冷哼從身側(cè)響起绿渣,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎燕耿,沒想到半個月后中符,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡誉帅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年淀散,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚜锨。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡档插,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出亚再,到底是詐尸還是另有隱情,我是刑警寧澤氛悬,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布则剃,位于F島的核電站,受9級特大地震影響如捅,放射性物質(zhì)發(fā)生泄漏棍现。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一伪朽、第九天 我趴在偏房一處隱蔽的房頂上張望轴咱。 院中可真熱鬧,春花似錦、人聲如沸朴肺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽戈稿。三九已至西土,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鞍盗,已是汗流浹背需了。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留般甲,地道東北人肋乍。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像敷存,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子觅闽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

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