成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

先有蛋還是先有雞?JavaScript 作用域與閉包探析

elisa.yang / 456人閱讀

摘要:而閉包的神奇之處正是可以阻止事情的發(fā)生。拜所聲明的位置所賜,它擁有涵蓋內(nèi)部作用域的閉包,使得該作用域能夠一直存活,以供在之后任何時(shí)間進(jìn)行引用。依然持有對(duì)該作用域的引用,而這個(gè)引用就叫閉包。

引子

先看一個(gè)問(wèn)題,下面兩個(gè)代碼片段會(huì)輸出什么?

// Snippet 1
a = 2;
var a;
console.log(a);

// Snippet 2
console.log(a);
var a = 2;

如果了解過(guò) JavaScript 變量提升相關(guān)語(yǔ)法的話,答案是顯而易見(jiàn)的。本文作為《你不知道的 JavaScript》第一部分的閱讀筆記,順便來(lái)總結(jié)一下對(duì)作用域與閉包的理解。

一、先有蛋還是先有雞

上面問(wèn)題的答案是:

-> 2

-> undefined

我們從編譯器的角度思考:

引擎會(huì)在解釋 JavaScript 代碼之前首先對(duì)其進(jìn)行編譯(沒(méi)錯(cuò),JavaScript 也是要進(jìn)行編譯的?。?,而編譯階段中的一部分工作就是找到所有聲明,并用合適的作用域?qū)⑺麄冴P(guān)聯(lián)起來(lái),即 包括變量和函數(shù)在內(nèi)的所有聲明都會(huì)在任何代碼被執(zhí)行前首先被處理。

當(dāng)你看到 var a = 2;時(shí)可能會(huì)認(rèn)為這是一個(gè)聲明,但 JavaScript 實(shí)際上會(huì)將其看成兩個(gè)聲明:var aa = 2,第一個(gè)定義聲明是在編譯階段進(jìn)行的,第二個(gè)賦值聲明會(huì)被留在原地等待執(zhí)行階段處理。

打個(gè)比方,這個(gè)過(guò)程就好像變量和函數(shù)聲明從它們的代碼中出現(xiàn)的位置被“移動(dòng)”到了最上面,這個(gè)過(guò)程就叫做 提升。

所以,編譯之后上面兩個(gè)代碼片段是這樣的:

// Snippet 1 編譯后
var a;
a = 2;
console.log(a);    // -> 2

// Snippet 2 編譯后
var a;
console.log(a);    // -> undefined
a = 2;

所以結(jié)論就是:先有蛋(聲明),后有雞(賦值)。

二、編譯

實(shí)際上,JavaScript 也是一門(mén)編譯語(yǔ)言。與傳統(tǒng)編譯語(yǔ)言的過(guò)程一樣,程序中的一段源代碼在執(zhí)行之前會(huì)經(jīng)過(guò)是三個(gè)步驟,統(tǒng)稱為“編譯”:

分詞/詞法分析(Tokenizing/Lexing)

解析/語(yǔ)法分析(Parsing)

代碼生成

簡(jiǎn)單來(lái)說(shuō),任何 JavaScript 代碼片段在執(zhí)行前都要進(jìn)行編譯(通常就在執(zhí)行前)。

三、作用域

為了理解作用域,可以想象出有以下三種角色:

引擎:從頭到尾負(fù)責(zé)整個(gè) JavaScript 程序的編譯及執(zhí)行過(guò)程。

編譯器:引擎的好朋友之一,負(fù)責(zé)語(yǔ)法分析及代碼生成等臟活累活。

作用域:引擎的另一位好朋友,負(fù)責(zé)收集并維護(hù)所有聲明的標(biāo)識(shí)符(變量)組成的一系列查詢,并實(shí)施一套非常嚴(yán)格的規(guī)則,確定當(dāng)前執(zhí)行的代碼對(duì)這些標(biāo)識(shí)符的訪問(wèn)權(quán)限。

var a = 2; 為例,過(guò)程如下:

首先遇到 var a,編譯器會(huì)詢問(wèn)作用域是否已經(jīng)有一個(gè)名為 a 的變量存在于同一個(gè)作用域的集合中。如果是,編譯器會(huì)忽略該聲明,繼續(xù)進(jìn)行編譯;否則就會(huì)要求作用域在當(dāng)前作用域的集合中聲明一個(gè)新的變量,并命名為 a.

然后,編譯器會(huì)為引擎生成運(yùn)行時(shí)所需的代碼,這些代碼被用來(lái)處理 a=2 這個(gè)賦值操作。引擎運(yùn)行時(shí)會(huì)首選詢問(wèn)作用域,在當(dāng)前的作用域集合中是否存在一個(gè)叫做 a 的變量。如果是,引擎就會(huì)使用這個(gè)變量;如果否,引擎就會(huì)繼續(xù)查找該變量(一層一層向上查找)。

最后,如果引擎最終找到了a變量,就會(huì)將 2 賦值給它,否則引擎就會(huì)舉手示意并拋出一個(gè)異常(ReferenceError)!

當(dāng)一個(gè)塊或函數(shù)嵌套在另一個(gè)塊或函數(shù)中時(shí),就發(fā)生了作用域的嵌套。遍歷嵌套作用域鏈的規(guī)則很簡(jiǎn)單:引擎從當(dāng)前的執(zhí)行作用域開(kāi)始查找變量,如果找不到,就向上一級(jí)查找。當(dāng)?shù)诌_(dá)最外層的全局作用域時(shí),無(wú)論找到還是沒(méi)找到,查找過(guò)程都會(huì)停止。

四、函數(shù)聲明式 & 函數(shù)表達(dá)式

JavaScript 中創(chuàng)建函數(shù)有兩種方式:

// 函數(shù)聲明式 
function funcDeclaration() { 
    return "A function declaration"; 
} 

// 函數(shù)表達(dá)式 
var funcExpression = function () { 
    return "A function expression"; 
}

聲明式與表達(dá)式的差異:

類似于 var 聲明,函數(shù)聲明可以 提升 到其它代碼之前,但函數(shù)表達(dá)式不能,不過(guò)允許保留在本地變量范圍內(nèi);

函數(shù)表達(dá)式可以匿名,而函數(shù)聲明不可以。

怎么判斷是函數(shù)聲明式還是函數(shù)表達(dá)式?

一個(gè)最簡(jiǎn)單的方法是看 function 關(guān)鍵字出現(xiàn)在聲明的位置,如果是在第一個(gè)詞,那么就是函數(shù)聲明式,否則就是函數(shù)表達(dá)式。

函數(shù)表達(dá)式比函數(shù)聲明式更加有用的地方:

是一個(gè)閉包

可以作為其他函數(shù)的參數(shù)

可以作為立即調(diào)用函數(shù)表達(dá)式(IIFE

可以作為回調(diào)函數(shù)

五、匿名函數(shù) & 立即調(diào)用函數(shù)

“在任意代碼片段外部添加包裝函數(shù),可以將內(nèi)部的變量和函數(shù)定義“隱藏起來(lái)”,外部作用域就無(wú)法訪問(wèn)包裝函數(shù)內(nèi)部的任何內(nèi)容。那么,能否更徹底一些?如果必須聲明一個(gè)有具體名字的函數(shù),這個(gè)名字本身就會(huì)“污染”所在作用域;其次,必須顯式通過(guò)函數(shù)名調(diào)用這個(gè)函數(shù)才能運(yùn)行其中的代碼。如果函數(shù)不需要函數(shù)名(或者至少函數(shù)名可以不污染所在作用域),并且能夠自動(dòng)運(yùn)行,這就完美了!”——論匿名函數(shù)和理解調(diào)用函數(shù)的誕生。

匿名函數(shù)表達(dá)式最熟悉的場(chǎng)景就是回調(diào)函數(shù):

setTimeout(function(){
    console.log("I waited 1 second!");
}, 1000);

匿名函數(shù)表達(dá)式書(shū)寫(xiě)起來(lái)簡(jiǎn)單快捷,很多庫(kù)和工具也傾向鼓勵(lì)使用這種風(fēng)格的代碼。但是,它也有幾個(gè)缺點(diǎn)需要考慮:

匿名函數(shù)在棧追蹤中不會(huì)顯示出有意義的函數(shù)名,使得調(diào)試很困難。

如果沒(méi)有函數(shù)名,當(dāng)函數(shù)需要引用自身時(shí)只能使用已經(jīng)過(guò)期的 arguments.callee 引用,比如在遞歸中。另一個(gè)函數(shù)需要引用自身的例子,是在事件觸發(fā)后事件監(jiān)聽(tīng)器需要解綁自身。

匿名函數(shù)省略了對(duì)于代碼可讀性、可理解性很重要的函數(shù)名。一個(gè)描述性的名稱可以讓代碼不言自明。

所以,始終給函數(shù)表達(dá)式命名是一個(gè)最佳實(shí)踐:

setTimeout(function timeoutHandler(){
    console.log("I waited 1 second!");
});

由于函數(shù)被包含在一對(duì)()括號(hào)內(nèi)部,因此成為了一個(gè)表達(dá)式,通過(guò)在末尾加上另外一個(gè)()括號(hào)就可以立即執(zhí)行這個(gè)函數(shù),比如:

(function foo(){
    // ...
})()

第一個(gè)()將函數(shù)變成了表達(dá)式,第二個(gè)()執(zhí)行了這個(gè)函數(shù)。

它有個(gè)術(shù)語(yǔ):IIFE,表示:立即執(zhí)行函數(shù)表達(dá)式(Immediately Invoked Function Expression)。

它有另外一個(gè)改進(jìn)形式:

(function foo(){
    // ...
}())    

不同點(diǎn)就是把最后的括號(hào)挪進(jìn)去了,實(shí)際上 這兩種形式在功能上是一致的,選擇哪個(gè)全憑個(gè)人喜好。

至于 IIFE 的另一個(gè)非常普遍的進(jìn)階用法是 把它們當(dāng)做函數(shù)調(diào)用并傳遞參數(shù)進(jìn)去

var a = 2;
(function foo(global){
    var a = 3;
    console.log(a);    // -> 3
    console.log(global.a);    // -> 2
})(window);    // 傳入window對(duì)象的引用
console.log(a);    // -> 2
六、再談提升

現(xiàn)在我們?cè)賮?lái)談一談提升。

// Snippet 3
foo();    // -> TypeError
bar();    // -> ReferenceError
var foo = function bar(){
    console.log(1);
};

為什么會(huì)輸出上面這兩個(gè)異常?我們可以從編譯器的角度把代碼看出這樣子:

var foo;    // 聲明提升
foo();      // 聲明但未定義為 undefined,然后這里進(jìn)行了函數(shù)調(diào)用,所以返回 TypeError
bar();      // 無(wú)聲明拋出引用異常,所以返回 ReferenceError
foo = function bar(){
    console.log(1);    
};

然后再變化一下,同名的函數(shù)聲明和變量聲明在提升階段會(huì)怎么處理:

foo();    // 到底會(huì)輸出什么?
var foo;
function foo(){
    console.log(1);
}
foo = function(){
    console.log(2);
}

上面代碼會(huì)被引擎理解為如下形式:

function foo(){
    console.log(1);
}
foo();    // -> 1
foo = function(){
    console.log(2);
}

解釋:var foo 盡管出現(xiàn)在 function foo() 的聲明之前,但它是重復(fù)的聲明(因此被忽略了),因?yàn)楹瘮?shù)聲明會(huì)被提升到普通變量之前。即:函數(shù)聲明和變量聲明都會(huì)被提升,但函數(shù)會(huì)首先被提升,然后才是變量(這也從側(cè)面說(shuō)明了在 JavaScript 中“函數(shù)是一等公民”)。

再來(lái):

foo();    // -> 3
function foo(){
    console.log(1);
}
var foo = function(){
    console.log(2);
}
function foo(){
    console.log(3);
}

解釋:盡管重復(fù)的 var 聲明會(huì)被忽略掉,但出現(xiàn)后面的函數(shù)聲明還是可以覆蓋前面的。

七、閉包

閉包是基于詞法作用域書(shū)寫(xiě)代碼時(shí)所產(chǎn)生的自然結(jié)果,你甚至不需要為了利用它們而有意識(shí)地創(chuàng)建閉包。閉包的創(chuàng)建和使用在你的代碼中隨處可見(jiàn)。你缺少的是根據(jù)你自己的意愿來(lái)識(shí)別、擁抱和影響閉包的思維環(huán)境。

當(dāng)函數(shù)可以記住并訪問(wèn)所在的詞法作用域時(shí),就產(chǎn)生了 閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行。

function foo(){
    var a = 2;
    function bar(){
        console.log(a);
    }
    return bar;
}
var baz = foo();
baz();    // -> 2,閉包的效果!

以下是解釋說(shuō)明:

函數(shù) bar() 的詞法作用域能夠訪問(wèn) foo() 的內(nèi)部作用域,然后我們將 bar() 函數(shù)本身當(dāng)做一個(gè)值類型緊傳遞。在這個(gè)例子中,我們將 bar() 所引用的函數(shù)對(duì)象本身當(dāng)做返回值。

foo()執(zhí)行后,其返回值(也就是內(nèi)部的 bar() 函數(shù))賦值給變量 baz 并調(diào)用 baz(),實(shí)際上只是通過(guò)不同的標(biāo)識(shí)符引用調(diào)用了內(nèi)部的函數(shù) bar()。

bar() 顯示是可以被正常執(zhí)行,但是在這個(gè)例子中,它在自己定義的詞法作用域以外的地方執(zhí)行。

foo() 執(zhí)行后,通常會(huì)期待 foo() 的整個(gè)內(nèi)部作用域都被銷毀,因?yàn)槲覀冎酪嬗欣厥掌饔脕?lái)釋放不再使用的內(nèi)存空間。由于看上去 foo() 的內(nèi)容不會(huì)再被使用,所以很自然地會(huì)考慮對(duì)其進(jìn)行回收。

而閉包的神奇之處正是可以阻止事情的發(fā)生。事實(shí)上,內(nèi)部作用域依然存在,因此沒(méi)有被回收。誰(shuí)在使用這個(gè)內(nèi)部作用域?原來(lái)是 bar() 本身在使用。

bar() 所聲明的位置所賜,它擁有涵蓋 foo() 內(nèi)部作用域的閉包,使得該作用域能夠一直存活,以供 bar() 在之后任何時(shí)間進(jìn)行引用。

bar() 依然持有對(duì)該作用域的引用,而 這個(gè)引用就叫閉包。

本質(zhì)上,無(wú)論何時(shí)何地,如果將函數(shù)(訪問(wèn)它們各自的詞法作用域)當(dāng)做第一級(jí)的值類型并到處傳遞,你就會(huì)看到閉包在這些函數(shù)中的應(yīng)用。在定時(shí)器、事件監(jiān)聽(tīng)器、Ajax 請(qǐng)求、跨窗口通信、Web Workers 或者任何其他的異步(或者同步)任務(wù)中,只要使用了回調(diào)函數(shù),實(shí)際上就是在使用閉包。

再補(bǔ)充一個(gè)示例:

function foo() {
    function bar() {
        console.log("1");
    }
    function baz() {
        console.log("2");
    }

    var yyy = {
        bar: bar,
        baz: baz
    }
    return yyy;
}

var kkk = foo();    // kkk通過(guò)foo獲得了yyy的引用,也就可以調(diào)用bar和baz
        
kkk.bar();    // -> 1
kkk.baz();    // -> 2
九、動(dòng)態(tài)作用域

事實(shí)上,JavaScript 并不具有動(dòng)態(tài)作用域,它只有 詞法作用域(雖然 this 機(jī)制某種程度上很像動(dòng)態(tài)作用域)。詞法作用域和動(dòng)態(tài)作用域的主要區(qū)別為:

詞法作用域是在寫(xiě)代碼或者定義時(shí)確定的,而動(dòng)態(tài)作用域是在運(yùn)行時(shí)確定的;

詞法作用域關(guān)注函數(shù)在何處聲明,而動(dòng)態(tài)作用域關(guān)注函數(shù)從何處調(diào)用。

像下面的代碼片段,如果是動(dòng)態(tài)作用域輸出的就是3而不是2了:

function foo(){
    console.log(a);    // -> 2
}
function bar(){
    var a = 3;
    foo();
}
var a = 2;
bar();
十、參考

你不知道的 JavaScript(上卷)

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/88139.html

相關(guān)文章

  • 你不知道的提升 - 先有還是有蛋?

    摘要:一先有雞還有先有蛋直覺(jué)上會(huì)認(rèn)為代碼在執(zhí)行時(shí)是由上到下一行一行執(zhí)行的。不幸的是兩種猜測(cè)都是不對(duì)的。換句話說(shuō),我們的問(wèn)題先有雞還是先有蛋的結(jié)論是先有蛋聲明后有雞賦值。 一、先有雞還有先有蛋? 直覺(jué)上會(huì)認(rèn)為javascript代碼在執(zhí)行時(shí)是由上到下一行一行執(zhí)行的。但實(shí)際上這并不完全正確,有一種特殊情況會(huì)導(dǎo)致這個(gè)假設(shè)是錯(cuò)誤的。 a = 2; var a; console.log(a); 大家...

    fish 評(píng)論0 收藏0
  • JavaScript 原型中的哲學(xué)思想

    摘要:而作為構(gòu)造函數(shù),需要有個(gè)屬性用來(lái)作為以該構(gòu)造函數(shù)創(chuàng)造的實(shí)例的繼承。 歡迎來(lái)我的博客閱讀:「JavaScript 原型中的哲學(xué)思想」 記得當(dāng)年初試前端的時(shí)候,學(xué)習(xí)JavaScript過(guò)程中,原型問(wèn)題一直讓我疑惑許久,那時(shí)候捧著那本著名的紅皮書(shū),看到有關(guān)原型的講解時(shí),總是心存疑慮。 當(dāng)在JavaScript世界中走過(guò)不少旅程之后,再次萌發(fā)起研究這部分知識(shí)的欲望,翻閱了不少書(shū)籍和資料,才搞懂...

    sugarmo 評(píng)論0 收藏0
  • 如何優(yōu)雅的理解ECMAScript中的對(duì)象

    摘要:標(biāo)準(zhǔn)對(duì)象,語(yǔ)義由本規(guī)范定義的對(duì)象。這意味著雖然有,本質(zhì)上依然是構(gòu)造函數(shù),并不能像那樣表演多繼承嵌套類等高難度動(dòng)作。不過(guò)這里的并不是我們所說(shuō)的數(shù)據(jù)類型,而是對(duì)象構(gòu)造函數(shù)。 序 ECMAScript is an object-oriented programming language for performing computations and manipulating computat...

    why_rookie 評(píng)論0 收藏0
  • 原型鏈?zhǔn)鞘裁矗筷P(guān)于原型鏈中constructor、prototype及__proto__之間關(guān)系的認(rèn)

    摘要:的隱式原型是母,母是由構(gòu)造函數(shù)構(gòu)造的,但函數(shù)的隱式原型又是。。。。可能是考慮到它也是由構(gòu)造函數(shù)生成的吧,所以返回的值也是。 showImg(https://segmentfault.com/img/bVyLk0); 首先,我們暫且把object類型和function類型分開(kāi)來(lái),因?yàn)?function是一個(gè)特殊的對(duì)象類型,我們這里這是便于區(qū)分,把function類型單獨(dú)拿出來(lái)。順便一提,...

    kaka 評(píng)論0 收藏0
  • YouDontKnowJS 小黃書(shū)學(xué)習(xí)小結(jié)

    摘要:真正的理解閉包的原理與使用更加透徹綁定的四種規(guī)則機(jī)制你不知道的人稱小黃書(shū),第一次看到這本書(shū)名就想到了一句話你懂得,翻閱后感覺(jué)到很驚艷,分析的很透徹,學(xué)習(xí)起來(lái)也很快,塊級(jí)作用域語(yǔ)句語(yǔ)句相當(dāng)于比較麻煩而且用在對(duì)象上創(chuàng)建的塊作用域僅僅在聲明中有效 真正的理解閉包的原理與使用 更加透徹this綁定的四種規(guī)則機(jī)制 你不知道的JavaScript 人稱小黃書(shū),第一次看到這本書(shū)名 就想到了一句話...

    Yuqi 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<