JavaScript:做用域鏈、做用域

  本文主要涵蓋了做用域鏈、做用域等內容。javascript

  本文會涉及上下文、變量對象等內容,有不清楚的同窗能夠先看上篇文章前端

做用域鏈

  函數對象和其它對象同樣,擁有能夠經過代碼訪問的屬性和一系列僅供JavaScript引擎訪問的內部屬性。其中一個內部屬性是[[scope]],由ECMA-262標準第三版定義,該內部屬性包含了函數被建立的做用域中對象的集合,這個集合被稱爲函數的做用域鏈,它決定了哪些數據能被函數訪問。java

  上面是一段很官方的話,畢竟是官方寫的。固然官方說的很對,但有點難理解,下面我就依據上述的內容作補充。git

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

  再簡單點說就是內部上下文全部變量對象的列表。啥意思???看下面的實例一:編程

// 實例一
function foo() {
  var a = 1;
  function bar() {
    var b = 2;
    console.log(b);
  }
  bar();
}
foo();
複製代碼

  上面的實例一,全局代碼,foo函數、bar函數的執行上下文前後建立,每個執行上下文都包含變量對象this以及做用域鏈。其中bar函數的執行上下文:數組

barEC = {
  VO: {xxx}, // 變量對象
  this: xxx,
  scopeChain: [barContext.VO, fooContext.VO, globalContext.VO] // 做用域鏈
}
複製代碼

  函數的做用鏈一般維護在該函數的執行上下文中scopeChain屬性中,能夠直接用一個數組來表示做用域鏈,數組的第一項爲做用域鏈最前端,最前端是該函數的變量對象,數組的最後一項爲做用域鏈最末端,最末端爲全局變量對象。bash

  實例一中一共會常見三個執行上下文,bar函數的執行上下文位於執行上下棧的最頂端,因此其執行上下文的做用域鏈包括當前執行上下文的變量對象以及其商城環境的一系列的變量對象VO(foo)VO(global),因此bar函數的執行上下文的做用域鏈中有三個變量對象。閉包

  做用域鏈是一個鏈表,是一個線性表,也就是說當前做用域和上層做用域並非包含關係,是一個有方向的鏈式關係,而且是單向的,最前端是起點,最末端是終點。因此咱們能夠沿着這個單向的鏈表查詢變量對象中的標識符,這樣也就能夠訪問到上一層做用域的變量。同時也保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。接下來說講[[scope]]屬性。編程語言

[[Scope]]

  函數中有一個內部屬性[[scope]],當函數建立的時候,就會保存全部的父變量對象到其中,也就能夠理解[[scope]]就是全部父輩變量對象的層級鏈,可是[[scope]]並不表示完整的做用域鏈。看下面實例二:

// 實例二
function foo() {
  var a = 1;
  function bar() {
    var b = 2;
    console.log(b);
  }
  bar();
}
foo();
複製代碼

  函數建立時,foo函數和bar函數的[[scope]]

foo.[[scope]] = [
  globalContext.VO
]
bar.[[scope]] = [
  fooContext.VO, globalContext.VO
]
複製代碼

  從上面的實例二看到,各自函數的[[scope]]只包含了各自全部父輩變量對象,沒有把本身的變量對象存入,此時本身的變量對象還沒建立,因此[[scope]]並不表示完整的做用域鏈。

  當函數被激活時,進入函數執行上下文,也就建立了變量對象和做用域鏈,並會將本身的變量對象添加到做用域鏈的最前端。

barContext = {
  VO: {xxx}, // 變量對象
  this: xxx,
  scopeChain:  [barContext.VO].concat(bar.[[scope]])
}
// [barContext.VO].concat(bar.[[scope]]) => [barContext.VO, fooContext.VO, globalContext.VO]
複製代碼

  上面就是做用域鏈建立的整個過程,回過頭在看看剛開始的那個官方對做用域鏈的定義應該就很好理解了。

做用域

  做用域,收集並維護一張全部被聲明的標識符(變量)的列表,並對當前執行中的代碼如何訪問這些變量強制實施了一組嚴格規則。簡單的說就是經過標識符名稱查詢變量的一組規則,明肯定義瞭如何在某些位置存儲變量,以及如何在稍後找到這些變量。

  做用域決定了代碼區塊中的變量和其餘資源的可見性。做用域能夠理解爲是一個獨立的地盤,不會讓變量外泄、暴露出去。也就是說做用域最大的做用就是隔離變量,不一樣做用域下同名變量不會有衝突。

詞法做用域

  在編程語言中,做用域分爲兩種:一種是詞法做用域,另外一種是動態做用域。JavaScript中做用域是詞法做用域,詞法做用域也叫作靜態做用域,也就是在詞法分析的階段就被定義了,簡單的說就是在代碼書寫的時候就被定義了。而動態做用域是值在代碼被執行的時候才決定。看下面的實例三:

// 實例三
var a = 1;
function foo() {
  console.log(a); // 結果是啥??? => 1
}
function bar() {
  var a = 2;
  foo();
}
bar();
複製代碼

  JavaScript採用的是靜態做用域,輸出的1。執行foo函數,會先從foo函數內部查找是否有局部變量a,若是有,則當前局部變量a的值;若是沒有,就根據書寫的位置,查找上層的代碼,也就是到了全局做用域,也就是a等於1,因此會輸出1。

  若是JavaScript採用的是動態做用域,,輸出的會2。執行foo函數,依然會從foo函數內部查找是否有局部變量a,若是沒有,就從調用函數的做用域,也就是bar函數內部查找a變量,因此會輸出2.

  JavaScript採用的是靜態做用域,輸出的1。

全局做用域

  在全部函數聲明或者大括號以外定義的變量,都在全局做用域裏。

// 默認全局做用域
var str = 'Hello world';
複製代碼

  在全局做用域內的變量能夠在任何其餘做用域內訪問和修改。

var str = 'Hello world';
function foo() {
  console.log(str); // Hello world 'str'能夠在foo函數內訪問
  str = 'Hello javascript';
  console.log(str); // Hello javascript 'str'能夠在foo函數內訪問和修改
}
console.log(str); // Hello world
foo();
console.log(str); // Hello javascript
複製代碼

  全部未定義直接賦值的變量自動聲明爲擁有全局做用域。

function foo() {
  str = 'Hello world';
  var name = 'Hello javaScript';
}
foo();
console.log(str); // Hello world
console.log(name); // 'ReferenceError: name is not defined'
複製代碼

  儘可能能夠在全局做用域定義變量,可是不推薦這樣作,由於可能會引發命名衝突,兩個或者多個變量使用相同的變量名。

  若是定義變量時使用了const或者let,那麼在命名衝突時,會報錯,使用const或者let不容許變量重複聲明。

let str = 'Hello world';
let str = 'Hello javaScript'; // 'Error, thing has already been declared'
複製代碼

  若是定義變量使用的時var,是容許重複聲明的,第二次定義會覆蓋第一次定義。這樣會讓代碼的調試變得很難,是不可取的。

var str = 'Hello world';
var str = 'Hello javaScript';
console.log(str); // Hello javascript
複製代碼

  因此,儘可能不要使用全局變量,使用局部變量。

局部做用域

  局部做用域是相對全局做用域而言。在代碼某一個具體範圍內使用使用的變量均可以在局部做用域內定義。

  JavaScript中有兩種局部做用域:函數做用域和塊級做用域。

函數做用域

  函數做用域是指,屬於這個函數的所有變量均可以在整個函數的範圍內使用以及複用。在函數以外,沒法訪問到。

function foo() {
  var str = 'Hello world';
  console.log(str); // Hello world
}
foo();
console.log(str); // 'ReferenceError: str is not defined'
複製代碼

  函數內定義的變量在函數做用域中,並且這個函數被調用時都具備不一樣的做用域。這也就意味着具備相同名稱的變量能夠在不一樣的函數中使用。這是由於這些變量被綁定到它們各自具備不一樣做用域的相應函數,而且在其餘函數中不可訪問。

function foo() {
  var str = 'Hello world';
  function bar() {
    var str = 'Hello javaScript';
    console.log(str); // Hello javaScript
  }
  bar();
  console.log(str); // Hello world
}
foo();
複製代碼

  做用域是分層的,內層做用域能夠訪問外層做用域的變量,反之不行。

塊級做用域

  塊級做用域是對塊語句而言的。

  塊語句就是大括號{}中間的語句,如ifswitch條件語句或forwhile循環語句,在ES6以前,塊語句不會一個新的做用域。在塊語句中定義的變量將保留在它們已經存在的做用域中。

if(true) {
  // if條件語句塊不會建立新的做用域
  var str = 'Hello world'; // 在全局做用域中
}
console.log(str); // Hello world
複製代碼

  ES6後,能夠經過letconst聲明變量,會產生塊級做用域,所聲明的變量在指定塊的做用域外沒法被訪問。

if(true) {
  // if條件語句塊會建立新的做用域
  let str = 'Hello world';
}
console.log(str); // 'str is not defined'
複製代碼

做用域與執行上下文

  做用域和執行上下文這兩個概念比較容易混淆,容易誤認爲是相同的概念,事實並非。

  JavaScript的執行分爲兩階段,一是語法檢查,二是執行:

  • 語法檢查:
    • 詞法分析
    • 語法分析
    • 做用域規則肯定
  • 執行:
    • 建立執行上下文
    • 執行函數代碼
    • 垃圾回收

  從上面就能看出,做用域和執行上下文並不同,做用域在函數定義的時候就已經肯定了,不是在函數調用的時候肯定的,而執行上下文是在函數執行前建立的。

  做用域其實就是一張全部被聲明的標識符(變量)的列表,裏面沒有值,就是定義瞭如何在某些位置存儲變量,以及如何在稍後找到這些變量,而後代碼執行的時候能夠賦值給變量。要經過做用域相對應的執行上下文來獲取變量的值。

  同一做用域下,不一樣的調用會產生不一樣的執行上下文,繼而產生不一樣的變量的值。因此,做用域變量中的值是在執行的過程當中肯定的,而做用域是在函數建立時就肯定了。

  若是要查找一個做用域下某個變量的值,就須要找到這個做用域對應的執行上下文,再在其中尋找變量的值。

  做用域與執行上下文的最大區別就是:

  執行上下文是在運行時肯定的,隨時可能會變;做用域是在定義時就肯定了,而且不會改變。

  一個做用域下可能包含若干個上下文環境。有可能歷來沒有過上下文環境(函數歷來就沒有被調用過);有可能有過,如今函數被調用完畢後,上下文環境被銷燬了;有可能同時存在一個或多個(閉包)。

結語

  文章若有不正確的地方歡迎各位大佬指正,也但願有幸看到文章的同窗也有收穫,一塊兒成長!

——————————————本文首發於我的公衆號——————————————

最後,歡迎你們關注個人公衆號或者 github博客,一塊兒學習交流。
相關文章
相關標籤/搜索