講清楚之javascript做用域

什麼是做用域(Scope)?

做用域產生於程序源代碼中定義變量的區域,在程序編碼階段就肯定了。javascript 中分爲全局做用域(Global context: window/global )和局部做用域(Local Scope , 又稱爲函數做用域 Function context)。簡單講做用域就是當前函數的生成環境或者上下文,包含了當前函數內定義的變量以及對外層做用域的引用。 javascript

做用域:前端

做用域(Scope) -
window/global Scope 全局做用域
function Scope 函數做用域
Block Scope 塊做用域(ES6)
eval Scope eval做用域

做用域定義了一套規則,這套規則定義了引擎如何在當前做用域或嵌套做用域根,據標識符來查詢變量。反過來講N個做用域組成的做用域鏈決定了函數做用域內標識符查找後返回的值。java

因此做用域肯定了當前上下文內定義的變量的可見性,即子做用域能夠訪問到當前做用域內屬性、函數。而且做用域鏈(Scope Chain)也肯定了在當前上下文中查找標識符後返回的值。面試

圖片描述

Scope分爲Lexical Scope和Dynamic Scope。Lexical Scope正如字面意思,即詞法階段定義的Scope。換種說法,做用域是根據源代碼中變量和塊的位置,在詞法分析器(lexer)處理源代碼時設置。javascript 採用的就是詞法做用域。

做用域規則

做用域限制了函數內變量、函數的可訪問性。在函數內部申明的屬性、函數屬於該函數的私有屬性,不對函數外部代碼暴露,同時函數內部申明的嵌套函數繼承了對當前函數內屬性、函數的訪問權。具體規則以下:數組

  • 若是變量 a 在函數內部定義, 則函數內部其餘變量具備訪問變量 a 的權限,可是函數外部代碼沒有訪問變量 a 的權限。因此同一做用域內變量能夠相互訪問,即 a、b、c 在同一個做用域他們就能夠相互訪問。這就像雞媽媽有寶寶,雞寶寶能夠相互打鬧,其餘雞就不能跟他們打鬧了,爲何? 由於雞媽媽不允許~ o(^∀^)o 。
let a = 1
function foo () {
    let b = 1 + a
    let c = 2
    console.log(b) // 2
}
console.log(c) // error 全局做用沒法訪問到 c
foo()
  • 若是變量 a 在全局做用域下定義(window/global),則全局做用域下的局部做用域內的執行代碼或者說是表達式均可以訪問到變量 a 的值。局部變量裏的同名變量(a)會截斷對全局變量 a 的訪問。(這裏的變量 a 就至關因而飼養員,候飼養員會在合適的時候給雞兒們投食。可是農場主爲了節約成本,規定飼養員要就近給雞投食,當飼養員1離雞寶寶更近時其餘飼養員就不能千里迢迢跨過鴨綠江去餵雞了。)
let a = 1
let b = 2
function foo () {
    let b = 3
    function too () {
        console.log(a) // 1
        console.log(b) // 3
    }
    too()
}
foo()

再次強調 javascript 做用域會嚴格限制變量的可訪問範圍: 即根據源代碼中代碼和塊的位置,嵌套做用域擁有對被嵌套做用域(外層做用域)的訪問權限。(這一條規則說明整個農場是有規則的,不能反向的投食。)緩存

做用域鏈(Scope Chain)

做用域鏈,是由當前環境與上層環境的一系列做用域共同組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。閉包

上面解釋的稍微有些晦澀,對於我這樣大腦很差使的就須要在大腦裏重複的'讀'幾回才能明白。那麼做用域鏈是幹嗎的? 簡單的說做用域鏈就是管理函數申明是造成的做用域嵌套(依賴)關係,並在函數運行階段解析函數訪問標識符的模塊化

再簡單點解釋做用域鏈是幹嗎的:做用域鏈就是用來查找變量的,做用域鏈是由一系列做用域串聯起來的。函數

做用域鏈的訪問

在函數執行過程當中,每遇到一個變量,都會經歷一次標識符解析過程,以決定從哪裏獲取和存儲數據。該過程從做用域鏈頭部,也就是當前執行函數的做用域開始(下圖中從左向右),查找同名的標識符,若是找到了就返回這個標識符對應的值,若是沒找到繼續搜索做用域鏈中的下一個做用域,若是搜索完全部做用域都未找到,則認爲該標識符未定義。函數執行過程當中,每一個標識符值得解析都要經歷這樣的搜索過程。性能

圖片描述
爲了具象化分析問題,咱們能夠假設做用域鏈是一個數組(Scope Array),數組成員有一系列變量對象組成。咱們能夠在數組這個單向通道中,也就是上圖模擬從左向右查詢變量對象中的標識符,這樣就能夠訪問到上一層做用域中的變量了。直到最頂層(全局做用域),而且一旦找到,即中止查找。因此內層的變量能夠屏蔽外層的同名變量。想象一下若是變量不是按從內向外的查找,那整個語言設計會變得N複雜了(咱們須要設計一套複雜的雞寶寶找食物的規則)

仍是上面的栗子:

let a = 1
let b = 2
function foo () {
    let b = 3
    function too () {
        console.log(a) // 1
        console.log(b) // 3
    }
    too()
}
foo()

做用域嵌套結構是這樣的:

圖片描述

栗子中,當 javascript 引擎執行到函數 too 時, 全局、函數 foo、函數 too 的上下文分別會被建立。上下文內包含它們各自的變量對象和做用域鏈(注意: 做用域鏈包含可訪問到的上層做用域的變量對象,在上下文建立階段根據做用域規則被收集起來造成一個可訪問鏈),咱們設定他們的變量對象分別爲VO(global),VO(foo), VO(too)。而 too 的做用域鏈,則同時包含了這三個變量對象,因此 too 的執行上下文可以下表示:

too = {
    VO: {...},  // 變量對象
    scopeChain: [VO(too), VO(foo), VO(global)], // 做用域鏈
}

咱們直接用scopeChain來表示做用域鏈數組,數組的第一項scopeChain[0]爲做用域鏈的最前端(當前函數的變量對象),而數組的最後一項,爲做用域鏈的最末端(全局變量對象 window )。注意,全部做用域鏈的最末端都爲全局變量對象。

再舉個栗子:

let a = 1
function foo() {
    console.log(a)
}
function too() {
    let a = 2
    foo()
}
too() // 1

這個栗子若是對做用域的特色理解不透徹很容易覺得輸出是2。但其實最終輸出的是 1。 foo() 在執行的時候先在當前做用域內查找變量 a 。而後根據函數定義時的做用域關係會在當前做用域的上層做用域裏查找變量標識符 a,因此最後查到的是全局做用域的 a 而不是 foo函數裏面的 a 。

變量對象、執行上下文會在後面介紹。

閉包

JavaScript中,函數和函數聲明時的詞法做用域造成閉包。或者更通俗的理解爲閉包就是可以讀取其餘函數內部變量的函數,這裏把閉包理解爲函數內部定義的函數。

咱們來看個閉包的例子

let a = 1
function foo() {
  let a = 2
  function too() {
    console.log(a)
  }
  return too
}
foo()() // 2

這是一個閉包的栗子,一個函數執行後返回另外一個可執行函數,被返回的函數保留有對它定義時外層函數做用域的訪問權。foo()() 調用時依次執行了 foo、too 函數。too 雖然是在全局做用域裏執行的,可是too定義在 foo 做用域裏面,根據做用域鏈規則取最近的嵌套做用域的屬性 a = 2。

再拿農場的故事作好比。農場主發現還有一種方法會更節約成本,就是讓每一個雞媽媽做爲家庭成員的‘飼養員’, 從而改變了以前的‘飼養結構’。

從做用域鏈的結構能夠發現,javascript引擎在查找變量標識符時是依據做用域鏈依次向上查找的。當標識符所在的做用域位於做用域鏈的更深的位置,讀寫的時候相對就慢一些。因此在編寫代碼的時候應儘可能少使用全局代碼,儘量的將全局的變量緩存在局部做用域中。

不增強記憶很容記錯做用域與執行上下文的區別。代碼的執行過程分爲編譯階段和解釋執行階段。始終應該記住javascript做用域在源代碼的編碼階段就肯定了,而做用域鏈是在編譯階段被收集到執行上下文的變量對象裏的。因此做用域、做用域鏈都是在當前運行環境內代碼執行前就肯定了。這裏暫且不過多的展開執行上下文的概念,能夠關注後續文章。

閉包的一些優缺點

閉包的用處:

  • 用於保存私有屬性:將不須要對外暴露的屬性、函數保存在閉包函數父函數裏,避免外部操做對值的干擾
  • 避免局部屬性污染全局變量空間致使的命名空間混亂
  • 模塊化封裝,將對立的功能模塊經過閉包進去封裝,只暴露較少的 API 供外部應用使用

閉包的缺點:

  • 內存消耗:因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題。
  • 致使內存泄露:因爲IE的 js 對象和 DOM 對象使用不一樣的垃圾收集方法,所以閉包在IE中會致使內存泄露問題,也就是沒法銷燬駐留在內存中的元素。解決方法是,在退出函數以前,將不使用的局部變量所有刪除)。
編譯階段和解釋執行階段會在變量對象一節詳細介紹。

關於閉包會的一些其餘知識點在後面的章節裏也會有說起,盡請關注。

思考

最後,再來看一個面試題:

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

// 5 5 5 5 5

要求對上面的代碼進行修改,使其輸出'0 1 2 3 4'

這裏也涉及到做用域鏈的概念,固然跟 javascript 的執行機制也有關。修改方式有不少種,下面給出一種:

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }(i), 1000);
}

// 0 1 2 3 4

詳細原理分析會在javascript 執行機制一節詳細介紹。

相關文章
相關標籤/搜索