亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

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

elisa.yang / 516人閱讀

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

引子

先看一個問題,下面兩個代碼片段會輸出什么?

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

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

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

一、先有蛋還是先有雞

上面問題的答案是:

-> 2

-> undefined

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

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

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

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

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

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

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

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

二、編譯

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

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

解析/語法分析(Parsing)

代碼生成

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

三、作用域

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

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

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

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

var a = 2; 為例,過程如下:

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

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

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

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

四、函數(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á)式不能,不過允許保留在本地變量范圍內(nèi);

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

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

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

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

是一個閉包

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

至于 IIFE 的另一個非常普遍的進(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對象的引用
console.log(a);    // -> 2
六、再談提升

現(xiàn)在我們再來談一談提升。

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

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

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

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

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

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

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

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

再來:

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

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

七、閉包

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

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

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

以下是解釋說明:

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

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

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

foo() 執(zhí)行后,通常會期待 foo() 的整個內(nèi)部作用域都被銷毀,因為我們知道引擎有垃圾回收器用來釋放不再使用的內(nèi)存空間。由于看上去 foo() 的內(nèi)容不會再被使用,所以很自然地會考慮對其進(jìn)行回收。

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

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

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

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

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

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

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

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

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

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

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

像下面的代碼片段,如果是動態(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)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/88139.html

相關(guān)文章

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

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

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

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

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

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

    why_rookie 評論0 收藏0
  • 原型鏈?zhǔn)鞘裁??關(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類型分開來,因為 function是一個特殊的對象類型,我們這里這是便于區(qū)分,把function類型單獨拿出來。順便一提,...

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

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

    Yuqi 評論0 收藏0

發(fā)表評論

0條評論

elisa.yang

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<