您好,登錄后才能下訂單哦!
這篇文章主要介紹了JavaScript作用域鏈是什么及怎么使用的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇JavaScript作用域鏈是什么及怎么使用文章都會有所收獲,下面我們一起來看看吧。
作用域是一套規則,負責收集并維護由所有聲明的標識符(變量)組成的一系列查詢,并實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問權限。
用更好理解的話闡述作用域是什么,則是:
作用域首先是一套規則
該規則的用處是用來存儲變量,以及如何有限制的獲取變量。
用一個不一定完全恰當的形容來類比作用域就是,存在一個國際銀行,你將手里各國的貨幣存入其中,當你要取出一些錢時,它有一套規則限定你只能在可使用貨幣的當地才能取出相應的該貨幣。 這個銀行和它制定的規則,就是作用域對于變量的作用。
在JavaScript中,所使用的作用域是詞法作用域,也稱為靜態作用域
。它是在編譯前就確定的。JavaScript本質上是一門編譯型語言,只不過它編譯發生的時間節點是在代碼執行前的幾微秒。不同于其他編譯型語言是在構建的過程中編譯,所以才看上去更像是解釋型語言。
關于編譯和解釋暫且不論,只需要理解到詞法作用域就是靜態作用域。 理解靜態的含義就是當代碼在書寫下定義時就已經確定
了。也就是人所讀到代碼中變量和函數被定義在什么范圍,該范圍就是它們的作用域。
用一個簡單的例子來理解:
var a = 1function foo() { console.log(a) }function bar(b) { var a = 2 console.log(a) foo() function baz() { console.log(b) } return baz }var c = bar(a)c()
對于定義的三個函數foo, bar, baz以及變量a,它們在書寫時作用域就已經定義。
因而在代碼執行時, bar函數先調用傳入變量a的值, 在第一個輸出變量a值時,會先詢問自身作用域是否定義過變量a, 定義過則詢問是否存在a的值,存在著輸出變量a為2.
然后開始調用foo函數, foo中只有輸出變量a的值, 同樣也會詢問自身作用域
是否定義過變量a, foo中未定義, 則會往上尋找自身定義時的作用域
詢問是否定義過變量a, 全局作用域定義過并且存在a值, 因而輸出a為1。其實這其中已經涉及到了作用域鏈,但暫且不議。
之后進入到c函數調用也就是baz函數, baz中輸出變量b的值,b會詢問自身作用域是否存在定義過變量b, baz未定義, 則往上查找自身定義時的作用域
也就是bar函數作用域是否定義過變量b, bar實際隱含在參數中為變量b定義并且賦值為1, 因而最終輸出為1。
這就是靜態作用域,只需要看變量和函數的書寫位置,即可確定它們都作用域范圍。
與之相對的是動態作用域
, 在JavaScript中涉及到動態作用域的只有this指向,這在之后復習this時會涉及。
假設JavaScript是動態作用域,同樣看上述例子里的代碼執行過程。
bar先調用并傳入變量a, 在第一個輸出變量a值時, 完全獲取到變量a因而輸出2。在調用foo函數時, 由于自身作用域沒有變量a, 則會從自身被調用的位置的作用域
去往上查找,則此時為函數bar的作用域,因而輸出的a值為2。
c函數調用也就是baz函數調用時,也同樣是自身不存在變量b,去尋找自身被調用的位置的作用域
,也就是全局作用域,全局作用域中同樣未定義過變量b, 則直接報錯。
作用域的分類可以按照上述所說的靜動分為:
靜態作用域
動態作用域
在靜態作用域也就是詞法作用域中還可以按照一定的范圍細分為:
全局作用域
函數作用域
塊級作用域
全局作用域可以理解為所有作用域的祖先作用域, 它包含了所有作用域在其中。也就是最大的范圍。反向理解就是除了函數作用域和被{}花括號包裹起來的作用域之外,都屬于全局作用域
。
之所以在全局作用域外還需要函數作用域,主要是有幾個原因:
可以存在一個更小的范圍存放自身內部的變量和函數,外部無法訪問
由于外部無法訪問,所以相當于隱藏了內部細節,僅提供輸入和輸出,符合最小暴露原則
同時不同的函數作用域可以各自命名相同的變量和函數,而不產生命名沖突
函數作用域可以嵌套函數作用域,就像俄羅斯套娃一樣可以一層套一層,最終形成了作用域鏈
用一個例子來展示:
var name = 'xavier'function foo() { var name = 'parker' var age = 18 function bar() { var name = 'coin' return age } return bar() }foo()console.log(age) // 報錯
當代碼執行時, 最終會報錯表示age查找不到。 因為變量age是在foo函數中定義, 屬于foo函數作用域中, 驗證了第一點外部無法訪問內部。
而當代碼只執行到foo函數調用時, 其實foo函數有執行過程, 最終是返回了bar函數的調用,返回的結果應該是18。 在對于編寫代碼的人來說,其實只需要理解一個函數的作用是什么, 然后給一個需要的輸入,最后得出一個預期所想的輸出,而不需要在意函數內部到底是怎么編寫的。驗證了第二點只需要最小暴露原則。
在這代碼中, 對name變量定義過三次, 但每次都在各自的作用域中而不會產生覆蓋的結果。在那個作用域里調用,該作用域就會返回相應的值。這驗證了第三點規避命名沖突。
最終bar函數是在foo函數內部定義的,foo函數獲取不到bar內部的變量和函數,但是bar函數可以通過作用域鏈獲取到其父作用域也就是foo里的變量與函數。這驗證了第四點。
塊級作用域在ES6之后才開始普及,對于是var聲明的變量是無效的,僅對let和const聲明的變量有效。以{}包裹的代碼塊就會形成塊級作用域, 例如if語句, try/catch語句,while/for語句。但聲明對象不屬于。
let obj = { a: 1, // 這個區域不叫做塊級作用域} if (true) { // 這個區域屬于塊級作用域 var foo = 1 let bar = 2}console.log(foo) // 1console.log(bar) // 報錯
用一個大致的類比來形容全局作用域,函數作用域和塊級作用域。一個家中所有的范圍就稱為全局作用域,而家中的各個房間里的范圍則是函數作用域, 甚至可能主臥中還配套有單獨的衛生間的范圍也屬于函數作用域,擁有的半開放式廚房則是塊級作用域。
假設你要在家中尋找自己的貓,當它在客廳中,也就是全局作用域里,你可以立馬找到。但如果貓在房間里,而沒發出聲音。你在客廳中是無法判斷它在哪里,也就是無法找到它。這就是函數作用域。但是如果它在半開放式廚房里,由于未完全封閉,它是能跑出來的,所以你還是能找得到它。 反之你在房間里,如果它也在,那么可以直接找到。但如果你在房間而它在客廳中,則你可以選擇開門去客廳尋找,一樣也能找到。
上述的過程過于理論化,因而現在通過對于實質的情況也就是內存中的情況來討論。
之前上一篇說過在ES3中執行上下文都有三大內容:
變量對象
作用域鏈
this
實際在內存中,對于全局作用域來說,它所涵蓋的范圍就是全局對象GO。因為全局對象保存了所有關于全局作用域中的變量和方法。
而對于函數來說,當函數被調用時所創建出的函數執行上下文里的活動對象AO所涵蓋的范圍就是函數作用域, 并且函數本身存在有一個內部屬性[[scope]]
, 它是用來保存其父作用域的,而父作用域實際上也是另一個變量對象。
對于塊級代碼來說,就不能用ES3這套來解釋,而是用ES6中詞法環境和變量環境來解釋。塊級代碼會創建出塊級執行上下文,但塊級執行上下文里只存在詞法環境,不存在變量環境,因而這詞法環境里的環境記錄就是塊級作用域。
相同的解釋對于全局和函數也一樣,對于ES6中,它們執行上下文里的詞法環境和變量環境的環境記錄涵蓋的范圍就是它們的作用域。
用一段代碼來更好的理解:
var a = 'a'function foo() { let b = 'b' console.log(c) }if (true) { let a = 'c' var c = 'c' console.log(a) }foo()console.log(a)
對于這段代碼剛編譯完準備開始執行,也就是代碼創建時,此刻執行上下文棧和內存中的圖為:
當開始進行到if語句時,會創建塊級執行上下文,并執行完if語句時執行上下文棧和內存圖為:
當if語句執行完后, 就會被彈出棧,銷毀塊級執行上下文。然后開始調用foo函數,創建函數執行上下文,此時執行棧和內存圖為:
當foo執行時,變量b被賦值為'b',同時輸出c時會在自身環境記錄中尋找,但未找到,因而往上通過自身父作用域,也就是全局作用域的環境記錄中尋找,找到c的值為'c',輸出'c'。
通過上文闡述的各個知識點,作用域鏈就很好理解了,在ES3中就是執行上下文里其變量對象VO + 自身父作用域
,然后每個執行上下文依次串聯出一條鏈路所形成的就是作用域鏈。
而在ES6中就是執行上下文里的詞法環境里的環境記錄+外部環境引用
。外部環境引用依次串聯也會形成一條鏈路,也屬于作用域鏈。
它的作用在于變量的查找路徑
。當代碼執行時,遇到一個變量就會通過作用域鏈不斷回溯,直到找到該值又或者是到了全局作用域這頂層還是不存在,則會報錯。
以及之后關于閉包的產生,也是由于作用域鏈的存在所導致的。這會在之后的復習里涉及到。
var a = 10let b = 20const c = { d: 30}function foo() { console.log(a) let e = 50 return b + e a = 40}function bar() { console.log(f) var f = 60 let a = 70 console.log(f) return a + c.d}if (a <= 30) { console.log(a) let b = foo() console.log(b) } console.log(b) c.d = bar()console.log(a)console.log(c.d)
關于“JavaScript作用域鏈是什么及怎么使用”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“JavaScript作用域鏈是什么及怎么使用”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。