深入學習作用域和閉包—全面(JS系列之二)

作用域

在學習作用域之前塘安,先了解兩個重要的概念:編譯器、引擎

編譯器:負責詞法分析及代碼生成等編譯過程

引擎:負責整個 JavaScript 程序的編譯和執(zhí)行

什么是作用域

通俗的來講就是變量起作用的范圍社搅。比較規(guī)范的解釋(引用《你不知道的 JavaScript 》上卷),負責收集并維護由所有聲明的標識符(變量)組成的一系列查詢乳规,并實施一套非常嚴格的規(guī)則形葬,確定當前執(zhí)行代碼對這些標識符的訪問權(quán)限。

ES6之前暮的,JavaScript只有全局作用域函數(shù)作用域笙以,與其他類型語言不同的是它沒有塊級作用域。

if(true){
    var a = 1;//全局作用域
}
console.log(a); // 1

function foo(){
  var b = 1;//函數(shù)作用域
    console.log(a); //1
}
console.log(b); // ReferenceError 

在上面的代碼中冻辩,a 屬于全局作用域猖腕,if 后的花括號并沒有形成塊級作用域拆祈,而 b 屬于 foo 函數(shù)的作用域,在JavaScript中函數(shù)外部作用域訪問不到函數(shù)內(nèi)部作用域倘感,所以在全局作用域中訪問foo函數(shù)作用域變量b會報錯放坏。

es6之后,JavaScript 擁有了塊級作用域

if (true) {
    let a = 1
}
console.log(a)   // ReferenceError 

if老玛、for淤年、whiletry...catch 等在大括號中使用let逻炊、const 聲明的變量會形成塊級作用域互亮,如果在外部訪問會報錯。

作用域如何工作

變量提升

剛開始接觸 JavaScript 的同學可能會對變量先聲明后使用的現(xiàn)象十分不解余素,要理解它我們得了解JavaScript編譯的兩個原則:①編譯時聲明 ②運行時賦值

var a = 2;

//相當于↓
var a; //編譯時
a = 2; //運行時

上面這段代碼 var a = 2只做一件事豹休,對a進行賦值 ,不過瀏覽器引擎不這么看, 它會被分為 var aa = 2 兩步進行桨吊,一個在編譯器編譯時聲明變量威根,另一個在引擎運行時賦值。

編譯器首先將上面這段程序分解為詞法單元视乐,然后將詞法單元解析成一個樹結(jié)構(gòu)(AST抽象語法樹)洛搀。在開始代碼生成時,編譯器遇到var a佑淀,編譯器詢問作用域是否已經(jīng)聲明了這個變量留美;如果是,編譯器忽略該聲明伸刃,否則在當前作用域集合聲明一個新的變量谎砾,命名為a

引擎執(zhí)行a = 2首先詢問作用域捧颅,在當前的作用域集合中是否存在一個叫做a的變量景图。如果是,引擎就會使用這個變量碉哑,否則引擎會繼續(xù)延著作用域鏈查找該變量挚币。如果引擎最終找到了a變量,就會將 2 賦值給它扣典,否則引擎會拋出一個異常Uncaught ReferenceError: a is not defined

函數(shù)提升

a()  // aaa => 函數(shù)a被提升妆毕,所以在聲明前可以調(diào)用函數(shù)

var a
function a () {
console.log('aaa')
}

console.log(a) // ? a() {} 函數(shù)聲明優(yōu)先級比變量聲明高

var聲明的變量會提升,function 聲明的函數(shù)也會被提升贮尖,并且函數(shù)聲明優(yōu)先級比變量聲明優(yōu)先級高设塔,所以上面這段代碼打印 a 是個函數(shù),因為var a聲明的變量被function聲明的函數(shù)覆蓋了。

詞法作用域

詞法作用域就是定義在詞法階段的作用域闰蛔,也就是說作用域是在書寫代碼時函數(shù)聲明的位置來決定痕钢,與執(zhí)行過程無關(guān),JavaScript 采用的是詞法作用域序六。

相對詞法作用域另外一種叫做動態(tài)作用域任连,作用域是在執(zhí)行階段確定的,比如Bash腳本例诀、Perl語言等随抠。

看下面這段代碼示例:

var a = 1

function foo () {
console.log(a)
}
function bar () {
    var a = 'local'
    foo ()
}

bar() // 詞法作用域是:1 ;動態(tài)作用域是:‘local’


我們使用詞法作用域和動態(tài)作用域分析一下上面這段代碼執(zhí)行過程繁涂,bar 函數(shù)內(nèi)部調(diào)用 foo 函數(shù)

如果是詞法作用域拱她,調(diào)用 foo 查找變量a會從foo函數(shù)代碼定義的位置向外一層也就是全局作用域訪問,此時var a = 1,結(jié)果是 1扔罪;

如果是動態(tài)作用域秉沼,調(diào)用foo查找變量a會從當前調(diào)用函數(shù)位置開始向往搜索,發(fā)現(xiàn)外部聲明var a = 'local',所以 a的值是local;

而在JS引擎中上面這段代碼運行結(jié)果是 1矿酵,所以JavaScript采用的是詞法作用域

不過唬复,thisJavaScript 中比較特殊,JavaScript 程序在執(zhí)行的時候才會對this進行賦值全肮,在未執(zhí)行時不能知道this的作用域敞咧,所以比較準確的說在JavaScriptthis采用的是動態(tài)作用域。

修改詞法作用域: eval 和 with

eval 欺騙詞法作用域

eval 函數(shù)接收一個或多個聲明的代碼辜腺,會修改其所處的詞法作用域休建。


var a = 2
function foo (str, b) {
    eval(str) // 欺騙
    console.log(a, b)
}
foo('var a = 3', 1) // 3, 1

執(zhí)行 eval 函數(shù),傳入的字符串會解析成腳本執(zhí)行评疗,聲明一個變量 a 修改了 foo 函數(shù)的詞法作用域测砂,遮蔽了外部(全局)作用域中的同名變量訪問,欺騙了 foo 詞法作用域壤巷。另外,使用 eval 函數(shù)還容易受到xss攻擊瞧毙。

with 欺騙詞法作用域

with 將一個對象的引用當作作用域來處理胧华,將對象的屬性當作作用域中的標識符來處理,如果對象中沒有該標識號宙彪,會在全局創(chuàng)建一個新的詞法作用域

with 的用法

var obj = {
    a: 1,
    b: 2,
    c: 3
}
// 對象屬性賦值矩动,多次使用obj
obj.a = 2
obj.b = 3
obj.c = 4

// 使用 with 寫法簡潔
with(obj) {
    a = 3;
    b = 4;
    c = 5;
}

with 的缺陷

function foo(obj) {
    with(obj) {
        a = 2
    }
}
var obj1 = {
    a: 3
}
var obj2 = {
    b: 3
}
foo(obj1)
console.log(obj1.a) // 2

foo(obj2)
console.log(obj2.a) // undefined
console.log(a) // 2 —— a被泄露到了全局作用域上

with 會修改引用中屬性的值,如果引用中沒有該屬性释漆,在非嚴格模式下會在全局作用域中創(chuàng)建一個全新的詞法作用域悲没,欺騙了全局詞法作用域

除此之外,使用 evalwith 還會帶來性能問題男图,因為JS 引擎無法在編譯時對它們作用域進行查詢優(yōu)化示姿,這樣會導致代碼運行效率變慢甜橱,所以建議不要使用它們。

作用域鏈

作用域鏈形成是由詞法作用域和編譯時詞法環(huán)境對外部環(huán)境引用的結(jié)果栈戳,關(guān)于詞法環(huán)境外部環(huán)境的引用可以參考這篇文章【深入了解JavaScript執(zhí)行過程】
)

現(xiàn)在主要說說作用域鏈的構(gòu)成過程岂傲,開始執(zhí)行腳本時創(chuàng)建全局作用域,在全局環(huán)境調(diào)用 foo函數(shù) 時子檀,編譯foo 函數(shù)并創(chuàng)建foo函數(shù)作用域镊掖,foo 函數(shù)中聲明 bar函數(shù),在調(diào)用 bar函數(shù)會創(chuàng)建 bar 函數(shù)作用域褂痰。JavaScript中亩进,內(nèi)部函數(shù)可以訪問外部函數(shù)的變量,這樣缩歪, bar 函數(shù)作用域 =》 foo 函數(shù)作用域 =》全局作用域 構(gòu)成了一條作用域鏈归薛。


var a = 'global'
function foo () {
    var b = 'foo scoped'
    function bar () {
        var c = 'bar scoped'
        console.log(a, b, c)
        }
    bar()
    }
}


foo() // 'global'    'foo scoped'     'bar scoped'
    

閉包

談起閉包,它可是JavaScript兩個核心技術(shù)之一(異步和閉包),在面試以及實際應用當中驶冒,我們都離不開它們苟翻,甚至可以說它們是衡量js工程師實力的一個重要指標。下面我們就羅列閉包的幾個常見問題骗污,從回答問題的角度來理解和定義閉包崇猫。

問題如下:

- 什么是閉包

- 閉包的原理是什么

- 閉包是如何使用的

- 閉包的應用場景有哪些

如果你能回答上面這些問題,說明你對閉包非常熟悉了需忿;如果腦子里比較模糊回答不上來也不用擔心诅炉,繼續(xù)往下讀,相信你會找到答案的屋厘。

什么是閉包

網(wǎng)上有很多種對閉包解釋的說法:

1涕烧、閉包是由函數(shù)以及創(chuàng)建該函數(shù)的詞法環(huán)境組合而成

2、閉包是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)

讀起來比較抽象和拗口汗洒,用代碼來理解閉包议纯。

function foo() {
    var a = 2
    function bar () {
        console.log(a)
    }
    return bar
}
var baz = foo()

baz() // 2 —— 這就是閉包的效果

image

函數(shù)是一等公民,可以當成數(shù)值來使用溢谤,它既可以作為函數(shù)參數(shù)瞻凤,也可以作為函數(shù)返回值。調(diào)用foo函數(shù)返回bar世杀,理論上來說foo函數(shù)執(zhí)行完之后會被銷毀阀参,不過bar函數(shù)引用著fooa變量,所以執(zhí)行完foo瞻坝,函數(shù)體會被銷毀蛛壳,但是a被引用著不能被回收仍然保存在內(nèi)存當中,所以在外部調(diào)用bar函數(shù)可以訪到foo內(nèi)部函數(shù)的a變量。這時我們給foo起了另外一個名字叫閉包函數(shù)衙荐。

我們知道根據(jù)作用域鏈函數(shù)內(nèi)部可以訪問函數(shù)外部的變量捞挥,反過來是不行的,但是閉包可以做到赫模,這就是閉包的神奇之處

總結(jié)一下树肃,閉包本質(zhì)上是一個函數(shù),它返回另一個函數(shù)瀑罗,可以使外部函數(shù)可以訪問其他函數(shù)內(nèi)部的變量胸嘴。

閉包原理

細心的朋友可能知道答案了,閉包的原理就是詞法作用域和作用域鏈形成的結(jié)果斩祭。

如何使用閉包

為了能讓我們的程序更健壯劣像,我們往往需要將實現(xiàn)細節(jié)隱藏起來,只對外提供暴露接口摧玫,這也是面向?qū)ο笕筇匦灾环庋b性

私有變量

function foo () {
    var num = 0
    function bar () {
        ++num
        return num
    }
    return bar
}
var add1 = foo ()
add1() // 1
add1() // 2
add1() // 3
var add2 = foo ()
add2() // 1
add2() // 2
add2() // 3

每次執(zhí)行foo都得到相同的值耳奕,不會相互污染

function Person() {
    var age = 20
    var sex = 'man'
    getAge () {
        return age
    }
    setAge(value) {
        age = value
    }
    getSex () {
        return sex
    }
    setSex(value) {
        sex = value
    }
    return {
        getAge,
        setAge,
        getSex,
        setSex
    }
}

var zhangsan = Person()
zhangsan.getAge() // 20
zhangsan.getSex() // 男

隱藏實現(xiàn)細節(jié),對外暴露接口诬像。模擬實現(xiàn)了面向?qū)ο蟮乃枷胛萑海a也顯得健壯、易理解坏挠、可擴展可維護芍躏。

閉包的應用場景

定時器、事件監(jiān)聽器降狠、Ajax 請求对竣、跨窗口通信、Web Workers 或者任何其他的異步(或者同步)任務中榜配,只要使用了回調(diào)函數(shù)否纬,實際上就是使用閉包

閉包使用注意事項

1、閉包會使得函數(shù)中的變量都被保存在內(nèi)存中蛋褥,內(nèi)存消耗很大临燃,處理不當,容易造成內(nèi)存泄漏

2烙心、如果不是某些特定任務需要使用閉包膜廊,在其它函數(shù)中創(chuàng)建函數(shù)是不明智的,因為閉包在處理速度和內(nèi)存消耗方面對腳本性能具有負面影響弃理。

總結(jié)

寫的內(nèi)容有點多溃论,梳理一下

1屎蜓、首先講了什么是作用域痘昌,作用域類型分為全局作用域、函數(shù)作用域、函數(shù)作用域

2辆苔、其次作用域工作時算灸,使用varfunctioin聲明會出現(xiàn)變量提升和函數(shù)提升;JavaScript 是詞法作用域驻啤,evalwith 會欺騙詞法作用域

3菲驴、最后講了作用域鏈的原理和閉包使用介紹

引用鏈接

深入javascript——作用域和閉包

JavaScript中的作用域和閉包

從作用域鏈談閉包

【第863期】深入學習JavaScript閉包

推薦閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市骑冗,隨后出現(xiàn)的幾起案子赊瞬,更是在濱河造成了極大的恐慌,老刑警劉巖贼涩,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巧涧,死亡現(xiàn)場離奇詭異,居然都是意外死亡遥倦,警方通過查閱死者的電腦和手機谤绳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來袒哥,“玉大人缩筛,你說我怎么就攤上這事”こ疲” “怎么了瞎抛?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長粮呢。 經(jīng)常有香客問我婿失,道長,這世上最難降的妖魔是什么啄寡? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任豪硅,我火速辦了婚禮,結(jié)果婚禮上挺物,老公的妹妹穿的比我還像新娘懒浮。我一直安慰自己,他們只是感情好识藤,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布砚著。 她就那樣靜靜地躺著,像睡著了一般痴昧。 火紅的嫁衣襯著肌膚如雪稽穆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天赶撰,我揣著相機與錄音舌镶,去河邊找鬼柱彻。 笑死,一個胖子當著我的面吹牛餐胀,可吹牛的內(nèi)容都是我干的哟楷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼否灾,長吁一口氣:“原來是場噩夢啊……” “哼卖擅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起墨技,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤惩阶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扣汪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體琳猫,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年私痹,在試婚紗的時候發(fā)現(xiàn)自己被綠了脐嫂。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡紊遵,死狀恐怖账千,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情暗膜,我是刑警寧澤匀奏,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站学搜,受9級特大地震影響娃善,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瑞佩,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一聚磺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧炬丸,春花似錦瘫寝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至首启,卻和暖如春暮屡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背毅桃。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工褒纲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留愁溜,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓外厂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親代承。 傳聞我的和親對象是個殘疾皇子汁蝶,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

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