當前位置:
首頁 > 知識 > 想成為JS大牛,作用域是你必須知道的

想成為JS大牛,作用域是你必須知道的

原文:https://github.com/prettyEcho/deep-js/issues/3

我們常說,萬物都有其存在的價值,這話的確不錯,但是深思一下,是不是需要有個前提,萬物都在某些領域或多或少的存在某些價值。

舉個例子,汽車,絕對是個非常有價值的stuff,它給我們的日常出行,貨物運輸等帶來了極大的便利;筷子,同樣也是個非常有價值的stuff,它給我們吃飯帶來了極大的方便。但是,汽車能幫我們把菜送到嘴裡嗎?筷子能載著我們出行嗎?

那麼,我上面所說的某些領域,我們是不是可以稱其為作用域,我想是可以的。

說到這,那麼我就想問了:在JS里,作用域是不是也是類似的概念呢?

首先,我可以肯定的說這是一個在JavaScript中灰常灰常重要的概念,關係著JS里很多核心的機制,理解它,很多問題都迎刃而解了。

那麼,問問自己,在JS里,作用域是什麼?

心裡大概知道是什麼,但是細細一想又好像說不太清。

沒關係,下面我們就細細品味這個有意思的東東。

先throw概念吧:

作用域負責收集並維護由所有聲明的標識符(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問許可權。

通俗來說,作用域相當於一個管理員(有自己的一套規則),他負責管理所有聲明的標識符的有序查詢。

我們來講個故事,說說作用域到底幹了啥。


三兄弟齊上陣

long long ago,有3個關係很好的基友,老大叫引擎,老二叫編輯器,老三叫作用域。三兄弟眼看年歲已長,可手上還是沒有幾個銀子。個個都很著急,於是三兄弟謀劃一同做個事。

求職過程:此粗略去數萬個字。。。

最終他們做的工作是:負責JS的編譯和運行。

他們的工作內容是這樣的:

老闆甩給他們一項任務編譯並執行下面代碼:

開始工作:

編譯器:作用域,幫我看看你那有沒有儲存變數a。

作用域:二哥,還沒有。

編譯器:那好,幫我儲存一個。

引擎: 老三,你那有沒有一個叫做a的變數。

編譯器:大哥,還真有,剛二哥讓我存儲了一個。

引擎: 真是太好了,幫我拿出來,它的值是幾,我需要給它複製。

編譯器:大哥,它的值是2。

引擎: 謝謝你,三弟,這樣我就能列印它的值了。

上面講了一個不恰當的小故事,但是三者之間的關係大概就是這樣。


詞法作用域 VS 動態作用域

徹底搞懂JavaScript作用域里介紹過,大部分標準語言編譯器的第一個工作階段叫作詞法化(也叫單詞化)。回憶一下,詞法化的過程會對源代碼中的字元進行檢查,如果是有狀態的解析過程,還會賦予單詞語義。

在JS里,使用的作用域就是詞法作用域。

簡單地說,詞法作用域就是定義在詞法階段的作用域。換句話說,詞法作用域是由你在寫代碼時將變數和塊作用域寫在哪裡來決定的,因此當詞法分析器處理代碼時會保持作用域不變(大部分情況下是這樣的)。

在JS里,動態作用域和this機制息息相關。它的作用域詩是在運行的過程中確定的

從上面的代碼,我們可以看出:foo中列印a的值不是由寫代碼的位置確定的,而是取決於foo執行的位置。

區別

詞法作用域是在寫代碼或者說定義時確定的,而動態作用域是在運行時確定的。(this 也是!)

詞法作用域關注函數在何處聲明,而動態作用域關注函數從何處調用。


函數作用域

JS里,生成作用域的方式:

函數

with、eval (不建議使用,影響性能)

由此,我們知道JS里,絕大多數的作用域都是基於函數生成的。

每個函數都會為自身生成一個作用域氣泡。這個氣泡內所有的標識符都可以在這個氣泡中使用。

上面代碼,bar氣泡有標識符a、foo,因此在bar氣泡中可以訪問到a、foo; foo氣泡有標識符b,因此在bar氣泡中可以訪問到b; 當然還有一個全局氣泡,全局氣泡中有bar標識符,因此在全局氣泡中可以訪問到bar。

最小授權原則

最小授權原則是指在軟體設計中,應該最小限度地暴露必要內容,而將其他內容都「隱藏」起來,比如某個模塊或對象的 API 設計。

這個原則可以延伸到如何選擇作用域來包含變數和函數。如果所有變數和函數都在全局作 用域中,當然可以在所有的內部嵌套作用域中訪問到它們。但這樣會破壞前面提到的最小 特權原則,因為可能會暴漏過多的變數或函數,而這些變數或函數本應該是私有的,正確 的代碼應該是可以阻止對這些變數或函數進行訪問的。

例如:

在這個代碼片段中,變數 b 和函數 doSomethingElse(..) 應該是 doSomething(..) 內部具體 實現的「私有」內容。給予外部作用域對 b 和 doSomethingElse(..) 的「訪問許可權」不僅 沒有必要,而且可能是「危險」的,因為它們可能被有意或無意地以非預期的方式使用, 從而導致超出了 doSomething(..) 的適用條件。更「合理」的設計會將這些私有的具體內容隱藏在 doSomething(..) 內部,

例如:

現在,b 和 doSomethingElse(..) 都無法從外部被訪問,而只能被 doSomething(..) 所控制。 功能性和最終效果都沒有受影響,但是設計上將具體內容私有化了,設計良好的軟體都會 依此進行實現。

規避衝突

當我們的程序代碼逐漸多起來,難免會出現變數衝突。那麼如何規避衝突就顯得額外重要。

函數可以把標識符嚴謹的"隱藏"起來,外部無法訪問到,利用這個特性我們可以很好的規避衝突。

foo和bar中定義了相同的變數a,但是卻不會相互造成影響。因為函數可以很好的把標識符"隱藏"起來。

變數衝突的一個典型例子存在於全局作用域中。當程序中載入了多個第三方庫時,如果它 們沒有妥善地將內部私有的函數或變數隱藏起來,就會很容易引發衝突。 這些庫通常會在全局作用域中聲明一個名字足夠獨特的變數,通常是一個對象。這個對象 被用作庫的命名空間,所有需要暴露給外界的功能都會成為這個對象(命名空間)的屬 性,而不是將自己的標識符暴漏在頂級的詞法作用域中。

例如:

函數聲明 VS 函數表達式

函數聲明和函數表達式判別的依據是:函數的生命是否以function關鍵詞開始。 以關鍵詞function 開始的聲明是函數聲明,其餘的函數聲明全部是函數表達式。

具名函數 VS 匿名函數

具名函數 擁有名字的函數

需要注意:函數聲明一定要是具名函數。

匿名函數 沒有名字的函數

立即執行函數(IIFE)

該函數是以()開始,不是以關鍵詞function開始,因此IIFE是函數表達式

函數名對 IIFE 當然不是必須的,IIFE 最常見的用法是使用一個匿名函數表達式。雖然使 用具名函數的 IIFE 並不常見,但它具有以下優勢:

匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試很困難。

如果沒有函數名,當函數需要引用自身時只能使用已經過期的arguments.callee引用, 比如在遞歸中。另一個函數需要引用自身的例子,是在事件觸發後事件監聽器需要解綁 自身。

匿名函數省略了對於代碼可讀性/可理解性很重要的函數名。一個描述性的名稱可以讓 代碼不言自明。

因此具名函數的 IIFE 也是一個值得推廣的實踐。

這也是IIFE的一種表達方式,功能上和上面那種方式是一致的。選擇哪種全憑個人愛好。

IIFE 也可以和其他形式的函數一樣實現參數的傳遞(多說一句:參數傳遞是按值傳遞)。

這個模式的另外一個應用場景是解決 undefined 標識符的默認值被錯誤覆蓋導致的異常(雖 然不常見)。將一個參數命名為 undefined,但是在對應的位置不傳入任何值,這樣就可以 保證在代碼塊中 undefined 標識符的值真的是 undefined:

UMD (Universal Module Definition)

IIFE 還有一種變化的用途是倒置代碼的運行順序,將需要運行的函數放在第二位,在 IIFE 執行之後當作參數傳遞進去。儘管這種模式略顯冗長,但有些人認為它更易理解。


塊作用域

儘管函數作用域是最常見的作用域單元,當然也是現行大多數 JavaScript 中最普遍的設計 方法,但其他類型的作用域單元也是存在的,並且通過使用其他類型的作用域單元甚至可 以實現維護起來更加優秀、簡潔的代碼。

try...catch 非常少有人會注意到 JavaScript 的 ES3 規範中規定 try/catch 的 catch 分句會創建一個塊作用域, catch 的參數變數僅在 catch 內部有效。

ES6的標準使我們能夠簡單的創建塊作用域,其中一個變數定義方式是let關鍵詞定義。

let定義的變數具有以下的特點:

let隱形的創建塊作用域({...})

let聲明的變數不能進行變數提升,因此只能先定義,後使用

let一個典型的應用就是在for循環里

我們看下面兩個例子:

其原因就是let形成了5個塊作用域,使每次輸出的變數都從本次循環的塊作用域中獲取。

當然我們還可以有其他方式做到第二種效果,我們將在 閉包,是真的美中說道。

除了 let 以外,ES6 還引入了 const,同樣可以用來創建塊作用域變數,但其值是固定的 (常量)。之後任何試圖修改值的操作都會引起錯誤。


作用域鏈

作用域鏈是由當前作用域與上層一系列父級作用域組成,作用域的頭部永遠是當前作用域,尾部永遠是全局作用域。作用域鏈保證了當前上下文對其有權訪問的變數的有序訪問。

上面代碼是由3層作用域氣泡組成,foo氣泡中試圖列印變數a,引擎在foo氣泡中未找到a變數,於是去其父作用域氣泡bar中尋找...以此類推直到找到全局作用域氣泡,發現有變數a,將其值列印出來。如若沒找到,報ReferenceError錯誤。

END


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 JavaScript 的精彩文章:

改進 UI 微交互的實用建議
關於 ECMAScript 2015的一些有用的提示和技巧

TAG:JavaScript |