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