摘要:也毫不例外,但在中作用域的特性與其他高級語言稍有不同,這是很多學習者久久難以理清的一個核心知識點。主要使用的是函數作用域。
關于作用域:About Scope
作用域是程序設計里的基礎特性,是作用域使得程序運行時可以使用變量存儲值、記錄和改變程序的“狀態”。JavaScript 也毫不例外,但在 JavaScript 中作用域的特性與其他高級語言稍有不同,這是很多學習者久久難以理清的一個核心知識點。
定義:Definition首先引用兩處我認為比較精辟的對作用域定義的總結:
Scope is the accessibility of variables, functions, and objects in some particular part of your code during runtime. In other words, scope determines the visibility of variables and other resources in areas of your code.
翻譯:作用域是在運行時對代碼某些特定部分中的變量、函數和對象的可訪問性。換句話說,作用域決定代碼區域中變量和其他資源的可見性。
Scope is the set of rules that determines where and how a variable (identifier) can be looked-up.
翻譯:作用域是一套規則,決定變量定義在何處以及如何查找變量。
綜上所述,我們可以把作用域理解成是在一套在程序運行時控制變量訪問的管理機制。它規定了變量可見的區域、變量查找規則、嵌套時的檢索方法。
目的:Purpose利用作用域是為了遵循程序設計中的最小訪問原則,也稱最小特權原則,這是一種以安全性為考量的程序設計原則,可以便于快速定位錯誤,將發生錯誤時的損失控制在最低程度。這篇文章的這一部分舉了一個電腦管理員的例子來說明最小訪問原則在計算機領域的重要性。
在編程語言中,作用域還有另外兩個好處——規避變量名稱沖突和隱藏內部實現。
我們知道每個作用域具有自己的權利控制范圍,在不同的作用域中定義相同名稱的變量是完全可行的。實現這一可能性的底層機制叫做“遮蔽效益”。這一機制體在嵌套作用域下得到了更好的體現,因為變量查找的規則是逐級向上,遇到匹配則停止,當內外層都有同名變量的時候,如已在內層找到匹配的變量,就不會再繼續向外層作用域查找了,就像是內層的變量把外層的同名變量遮蔽住了一樣。是不是感覺非常熟悉?沒錯,這也是 JavaScript 中原型鏈查找的內部機制!
隱藏內部實現其實是一種編程的最佳實踐,因為只要編程者愿意,大可暴露出全部代碼的內部實現細節。但眾所周知,這是不安全的。如果第三者在不可控的情況下修改了正常代碼,影響程序的運行,這將帶來災難性的后果,這不僅是庫開發者們首先會考慮的安全性問題,也是業務邏輯開發者們需要謹慎對待的可能沖突,這就是模塊化之所以重要的原因。其他編程語言在語法特性層面就支持共有和私有作用域的概念,而 JavaScript 官方暫時還沒有正式支持。目前用以隱藏內部實現的模塊模式主要依賴閉包,所以閉包這一在JS領域具有獨特神秘性的機制被廣大開發者們又恨又愛。即便 ES6 的新模塊機制支持以文件形式劃分模塊,仍然離不開閉包。
生成:Generate作用域的生成主要依靠詞法定義,許多語言中有函數作用域和塊級作用域。JavaScript 主要使用的是函數作用域。怎么理解詞法定義作用域?詞法就是書寫規則,編譯器會按照所書寫的代碼確定出作用域范圍。
大多數編程語言里都用 {} 來包裹一些代碼語句,編譯器就會將它理解為一個塊級,它內部的范圍就是這個塊級的作用域,函數也是如此,寫了多少個函數就有相應數量的作用域。雖然 JavaScript 是少數沒有實現塊級作用域的編程語言,但其實在早期的 JavaScript 中就有幾個特性可以變相實現塊級作用域,如 with、catch 語句:with 語句會根據傳入的對象創建出一個特殊作用域,只在 with 中有效;而 catch 語句中捕捉到的錯誤變量在外部無法訪問的原因,正是因為它創建出了一個自己的塊級作用域,據 You Don"t Know JS 的作者說市面上支持塊級作用域書寫風格的轉譯插件或 CoffeeScript 之類的轉譯語言內部都是依靠 catch 來實現的,that"s so tricky!
相關概念:Relevant Concepts在這里只討論 JavaScript 中以下概念的內容和實現方式。
詞法作用域:Lexical Scope通過上面所說的相關知識可以總結出詞法作用域就是按照書寫時的函數位置來決定的作用域。
看看下面這段代碼,這段代碼展示了除全局作用域之外的 3 個函數作用域,分別是函數 a 、函數 b 、函數 c 所各自擁有的地盤:
function a () { var aa = "aa"; function b () { var bb = "bb" console.log(aa, bb) c(); } b(); } function c () { var cc = "cc" console.log(aa, bb, cc) } a();
各個變量所屬的作用域范圍是顯而易見的,但這段代碼的執行結果是什么呢?一但面臨嵌套作用域的情景,或許很多人又要猶疑了,接下來才是詞法作用域的重點。
上面代碼的執行結果如下所示:
// b(): aa bb // c(): Uncaught ReferenceError: aa is not defined
函數 c 的運行報錯了!錯誤說沒有找到變量 aa。按照函數調用時的代碼來看,函數 c 寫在函數 b 里,按道理來講,函數 c 不是應該可以訪問它嵌套的兩層父級函數作用域么?從執行結果得知,詞法作用域不關心函數在哪里調用,只關心函數定義在哪里,所以函數 c 其實直接存在全局作用域下,與函數 a 同級,它倆根本就是沒有任何交點的世界,無法互相訪問,這就是詞法作用域的法則!
請謹記 JavaScript 就是一個應用詞法作用域法則的世界。而按照函數調用時決定的作用域叫做動態作用域,在 JavaScript 里我們不關心它,所以把它扔出字典。
函數作用域:Function Scope很長時間以來,JavaScript 里只存在函數作用域(讓我們暫時忽略那些里世界的塊級作用域 tricky),所有的作用域都是以函數級別存在。對此做出最明顯反證的就是條件、循環語句。函數作用域的例子在上述詞法作用域中已經得到了很好的體現,就不再贅述了,這里主要探討一下函數作用域鏈的機制。
以下面一段代碼為例:
function c () { var cc = "cc" console.log(cc) } function a () { var aa = "aa" console.log(aa) b(); } function b () { var bb = "bb" console.log(aa, bb) } a(); c();
一個程序里可以有很多函數作用域,引擎怎么確定先從哪個作用域開始,按照詞法規則先寫先執行?當然不,這時就看誰先調用。函數在作用域中的聲明會被提升,函數聲明的書寫位置不會影響函數調用,參照上例,即便是函數 a 定義在函數 c 后面,由于它會被先調用,所以在全局作用域之后還是會先進入函數 a 的作用域,那函數 b 和函數 c 的順序又如何,為了解釋清楚詞法作用域是如何與函數調用機制結合起來,接下來要分兩部分研究程序運行的細節。
都說 JavaScript 是個動態編程語言,然而它的作用域查找規則又是按照詞法作用域(也是俗稱的靜態作用域)規則來決定的,實在讓人費解。理解它動(執行時編譯)靜(運行前編譯)結合的關鍵在于引擎在執行程序時的兩個階段:編譯和運行。為了避免歧義,區分了兩個詞:
執行:引擎對程序的整體執行過程,包括編譯、運行階段。
運行:具體代碼的執行或函數調用的過程。
JavaScript 的動指的是在程序被執行時才進行編譯,僅在代碼運行前。而一般語言是先經過編譯過程,隨后才會被執行的,編譯器與引擎執行是繼時性的。靜指函數作用域是根據編譯時按照詞法規則來確定的,不由調用時所處作用域決定。
簡單來說,函數的運行和其中變量的查找是兩套規則:函數作用域中的變量查找基于作用域鏈,而函數的調用順序依賴函數調用的背后機制——調用棧來決定。在編譯階段,編譯器收集了函數作用域的嵌套層級,形成了變量查找規則依賴的作用域鏈。函數調用棧使函數像棧的數據結構一樣排成隊列按照先進后出的規則先后運行,再根據JavaScript 的同步執行機制,得出正確的執行順序是:函數 a =>函數 b =>函數 c。最后再結合詞法作用域法則推斷出上面示例的執行結果僅僅是一句報錯提示:Uncaught ReferenceError: aa is not defined。把函數 b 引用的變量 aa 去掉,就可以得到完整的執行順序的展示。
塊級作用域:Block Scopelet、const 聲明的出現終于打破了 JavaScript 里沒有塊級作用域的規則,我們可以顯示使用塊級語法 {} 或隱式地與 let、const 相結合實現塊級作用域。
隱式(let、const 聲明會自動劫持所在作用域形成綁定關系,所以下例中并不是在 if 的塊級定義,而是在它的代碼塊內部創建了一個塊級作用域,注意在 if 的條件語句中 a 尚未定義):
if (a === "a") { let a = "a" console.log(a) } else { console.log("a is not defined!") }
顯式(顯式寫法揭露了塊級變量定義的真實所在):
// 普通寫法,稍顯啰嗦 if (true) { { let a = "a" ... } } // You Don"t Know JS的作者提倡的寫法,保持let聲明在最前,與代碼塊語句區分開 if (true) { { let a = "a" ... } } // 希望未來官方能支持的寫法 if (true) { let (a = "a") { ... } }
關于塊級作用域最后要關注的一個問題是暫時性死區,這個問題可以描述為:當提前使用了以 var 聲明的變量得到的是 undefined,沒有報錯,而提前使用以 let 聲明的變量則會拋出 ReferenceError。暫時性死區就是用來解釋這個問題的原因。很簡單,規范不允許在還沒有運行到聲明語句時就引用變量。來看一下根據官方非正式規范得出的解釋:
When a JavaScript engine looks through an upcoming block and finds a variable declaration, it either hoists the declaration to the top of the function or global scope (for var) or places the declaration in the TDZ (for let and const). Any attempt to access a variable in the TDZ results in a runtime error. That variable is only removed from the TDZ, and therefore safe to use, once execution flows to the variable declaration.
翻譯:當 JavaScript 引擎瀏覽即將出現的代碼塊并查找變量聲明時,它既把聲明提升到了函數的頂部或全局作用域(對于 var ),也將聲明放入暫時性死區(對于 let 和const)。任何想要訪問暫時性死區中變量的嘗試都會導致運行時錯誤。只有當執行流到達變量聲明的語句時,該變量才會從暫時性死區中移除,可以安全訪問。
另外,把 let 跟 var 聲明作兩點比較能更好排除其他疑惑。以下述代碼為例:
console.log(a); var a; console.log(b); let b;
變量提升:let 與 var 定義的變量一樣都存在提升。
默認賦值:let 與 var 聲明卻未賦值的變量都相當于默認賦值 undefined。
let 與 var 聲明提前引用導致的結果的區別僅僅是因為在編譯器在詞法分析階段,將塊級作用域變量做了特殊處理,用暫時性死區把它們包裹住,保持塊級作用域的特性。
全局作用域:Global Scope全局作用域仿佛是透明存在的,容易受到忽視,就像人們經常忘記身處氧氣包裹中一樣,變量無法超越全局作用域存在,人們也無法脫離地球給我們提供的氧氣圈。簡而言之,全局作用域就是運行時的頂級作用域,一切的一切都歸屬于頂級作用域,它的地位如同宇宙。
我們在所有函數之外定義的變量都歸屬于全局作用域,這個“全局”視 JavaScript 代碼運行的環境而定,在瀏覽器中是 window 對象,在 Node.js 里就是 global 對象,或許以后還會有更多其他的全局對象。全局對象擁有的勢力范圍就是它們的作用域,定義在它們之中的變量對所有其他內層作用域都是可見的,即共享,所以開發者們都非常討厭在全局定義變量,這繼承自上面所說的最小特權原則的思想,為安全起見,定義在全局作用域里的變量越少越好,于是一個叫做全局污染的話題由此引發。
全局作用域在運行時會由引擎創建,不需要我們自己來實現。
局部作用域:Local Scope與全局作用域相對的概念就是局部作用域,或者叫本地作用域。局部作用域就是在全局作用域之下創建的任何內層作用域,可以說我們定義的任何函數和塊級作用域都是局部作用域,一般在用來與全局作用域做區別的時候才會采用這種概括說法。在開發中,我們主要關心的是使用函數作用域來實現局部作用域的這一具體方式。
公有作用域:Public Scope公有作用域存在于模塊中,它是提供項目中所有其他模塊都可以訪問的變量和方法的范圍或命名空間。公私作用域的概念與模塊化開發息息相關,我們通常關心的是定義在公私作用域中的屬性或方法。
模塊化提供給程序更多的安全性控制,并隱蔽內部實現細節,但是要讓程序很好的實現功能,我們有訪問模塊內部作用域中數據的需要。從作用域鏈的查找機制可知,外層作用域是無法訪問內層作用域變量的,而JavaScript 中公私作用域的概念也不像其他編程語言中那么完整,不能通過詞法直接定義公有和私有作用域變量,所以閉包成為了模塊化開發中的核心力量。
閉包實現了在外層作用域中訪問內層作用域變量的可能,其方法就是在內層函數里再定義一個內層函數,用來保留對想要訪問的函數作用域的內存引用,這樣外層作用域就可以通過這個保留引用的閉包來訪問內層函數里的數據了。
通過下面兩段代碼的執行結果就能看出區別:
function a () { var aa = "aa" function b () { var bb = "bb" } b() console.log(bb) } a()
控制臺報錯:Uncaught ReferenceError: bb is not defined,因為函數 b 在運行完后就從執行棧里出棧了,其內存引用也被內存回收機制清理掉了。
function a () { var aa = "aa" function b () { var bb = "bb" return function c () { console.log(bb) } } var c = b() console.log(c()) } a()
而這段代碼中用變量 c 保留了對函數 b 中返回的函數 c 的引用,函數 c 又根據詞法作用域法則,能夠進入函數 b 的作用域查找變量,這個引用形成的閉包就被保存在函數 a 中變量 c 的值中,函數 a 可以在任何想要的時候調用這個閉包來獲取函數 b 里的數據。此時這個被返回的變量 bb 就成為了暴露在函數 a 的作用域范圍內,定義在函數 b 里的公有作用域變量。
更加通用的實現公有作用域變量或 API 的方式,稱為模塊模式:
var a = (function a () { var aa = "aa" function b () { var bb = "bb" console.log(bb) } return { aa: aa, b: b } })() console.log(a.aa) a.b()
使用閉包實現了一個單例模塊,輸出了共有變量 a.aa 和 共有方法也稱 API 的 a.b。
私有作用域:Private Scope相對于公有作用域,私有作用域是存在于模塊中,只能提供由定義模塊直接訪問的變量和方法的范圍或命名空間。要澄清一個關于私有作用域變量的的誤會,定義私有作用域變量,不一定是要完全避免被外部模塊或方法訪問,更多時候是禁止它們被直接訪問。大多時候可以通過模塊暴露出的公有方法來間接地訪問私有作用域變量,當然想不想讓它被訪問或者如何限制它的增刪改查就是開發者自己掌控的事情了。
接著上述公有作用域的實現,來看看私有作用域的實現。
var a = (function a () { var bb = "bb" var cc = "c" function b () { console.log(bb) } function c () { cc = "cc" console.log(cc) } return { b: b, c: c } })() a.b() a.c()
在模塊 a 中定義的屬性 bb 和 cc 都是無法直接通過引用來獲取的。但是模塊暴露的兩個方法 b 和 c,分別實現了一個查找操作和修改操作,間接控制模塊中上述兩個私有作用域變量。
作用域與This:Scope vs This在對作用域是什么的理解中,最大的一個誤區就是把作用域當作 this 對象。
一個鐵打的證據是函數作用域的確定是在詞法分析時,屬于編譯階段,而 this 對象是在運行時動態綁定到函數作用域里的。另一個更明顯的證據是當函數調用時,它們內部的 this 指的是全局對象,而不是函數本身, 想必所有開發者都踩過這一坑,能夠理解作用域與 this 本質上的區別。從這兩點就可以肯定決不能把作用域與 this 等同對待。
this 到底是什么?它跟作用域有很大關系,但具體留到以后再討論吧。在此之前我們先要與作用域成為好朋友。
參考文獻:ReferenceYou Don"t Know JS: Scope & Closures
Understanding Scope in JavaScript
Understanding ECMAScript 6
Everything you wanted to know about JavaScript scope
Understanding scope and visibility
JavaScript 的 this 原理
Stack的三種含義
TEMPORAL DEAD ZONE (TDZ) DEMYSTIFIED
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/108962.html
摘要:作用域鏈的作用就是做標示符解析。事件循環還有個明顯的特點單線程。早期都是用作開發,單線程可以比較好當規避同步問題,降低了開發門檻。單線程需要解決的是效率問題,里的解決思想是異步非阻塞。 0、前言 本人在大學時非常癡迷java,認為java就是世界上最好的語言,偶爾在項目中會用到一些javascript,但基本沒放在眼里。較全面的接觸javascript是在實習的時候,通過這次的了解發現...
摘要:最近剛剛看完了你不知道的上卷,對有了更進一步的了解。你不知道的上卷由兩部分組成,第一部分是作用域和閉包,第二部分是和對象原型。附錄詞法這一章并沒有說明機制,只是介紹了中的箭頭函數引入的行為詞法。第章混合對象類類理論類的機制類的繼承混入。 最近剛剛看完了《你不知道的 JavaScript》上卷,對 JavaScript 有了更進一步的了解。 《你不知道的 JavaScript》上卷由兩部...
摘要:關鍵字計算為當前執行上下文的屬性的值。毫無疑問它將指向了這個前置的對象。構造函數也是同理。嚴格模式無論調用位置,只取顯式給定的上下文綁定的,通過方法傳入的第一參數,否則是。其實并不屬于特殊規則,是由于各種事件監聽定義方式本身造成的。 this 是 JavaScript 中非常重要且使用最廣的一個關鍵字,它的值指向了一個對象的引用。這個引用的結果非常容易引起開發者的誤判,所以必須對這個關...
閱讀 1388·2021-11-22 09:34
閱讀 2591·2021-11-12 10:36
閱讀 1125·2021-11-11 16:55
閱讀 2340·2020-06-22 14:43
閱讀 1478·2019-08-30 15:55
閱讀 1989·2019-08-30 15:53
閱讀 1775·2019-08-30 10:50
閱讀 1232·2019-08-29 12:15