JS進擊之路:做用域

引言

幾乎全部的編程語言都有做用域的概念,那做用域到底指的是什麼呢?做用域就是編程語言在定義變量時,變量如何存儲、變量如何訪問的一套規則,不一樣的編程語言的規則大同小異,接下來就來看看這套規則是怎麼設定的編程

編譯原理

在傳統編譯語言中,在代碼執行以前都會有一個編譯過程:編程語言

  • 分詞/詞法分析:將代碼語句分解成有意義的代碼塊,又叫詞法單元。
  • 解析/語法分析:將詞法單元轉換一個逐級嵌套的具備語法規則的樹狀結構,又叫抽象語法樹(AST)
  • 代碼生成:解析AST並轉化成機器指令

和傳統編譯語言不太同樣,js的編譯和執行並非分開執行,大多數狀況都是編譯過程結束就會馬上執行,爲了在短期的編譯過程內達到較優性能,js引擎較通常編譯器更復雜,如今就讓來看js的編譯過程,簡單的以編譯var a = 2爲例:函數

  • 遇到var a,編譯器會詢問做用域是否已經有一個該名稱的變量存在於同一個做用域的集合中。若是是,編譯器會忽略該聲明,繼續進行編譯;不然它會要求做用域在當前做用域的集合中聲明一個新的變量a
  • 接下來編譯器會爲引擎生成運行時所需的代碼,這些代碼被用來處理a=2這個賦值操做。引擎運行時會首先詢問做用域,在當前的做用域集合中是否存在一個叫作a的變量。若是否,引擎就會使用這個變量;若是引擎最終找到了a,就會將2賦值給它。不然引擎就會拋出一個異常

詞法做用域

做用域通常有兩種工做模型,第一種是被大多數編程語言所採用的詞法做用域,另一種叫做動態做用域,如Bash腳本採用的就是動態做用域。詞法做用域就是定義在詞法階段的做用域,詞法做用域是由你在寫代碼時將變量和塊做用域寫在哪裏來決定的,由變量定義位置決定,而動態做用域則是由變量使用的位置來決定的。下面來看個例子:性能

function foo(a) {
  var b = a * 2;
  function bar(c) {
    console.log(a, b, c)
  }
  bar(b * 3)
}
foo(2)

首先來分析一下這裏一共存在幾個做用域?設計

  1. 全局做用域,裏面存在foo變量
  2. foo函數建立的做用域,裏面有a,b,bar變量
  3. bar函數建立的做用域,裏面有c變量

接下來再來分析一下變量的查找過程,引擎執行console.log()須要查找a、b、c三個變量的引用,首先從最裏面的bar()做用域開始找,引擎沒法找到a,所以會再往上到foo()做用域中找,在這裏找到了a,中止查找,對於b、c來講查找過程同樣。做用域查找始終從運行時最內層開始查找,逐級向外查找,直到碰見第一個匹配的變量爲止。code

函數做用域

函數做用域指的是屬於這個函數的所有變量均可以在整個函數的範圍內使用及複用,這是你們都知道的定義,可是函數做用域的存在到底有什麼用呢?接下來就一塊兒看看函數做用域的秒用。對象

隱藏內部實現

隱藏內部實現就是將變量和函數包裹在一個函數的做用域中,達到隱藏的目的,爲何要這麼作呢?軟件設計中有一個很是有名的原則叫最小暴露原則,指最小限度暴露必要內容,而將其餘內容都隱藏起來,好比模塊或對象的API設計。用函數做用域來包裹變量和函數來達到最小暴露原則,阻止外部直接訪問,來看下面的例子:ip

function doSomething(a) {
  b = a + doSomethingElse( a * 2 );
  console.log( b * 3 );
}
  
function doSomethingElse(a) {
  return a - 1;
}
var b;
doSomething( 2 ); // 15」

在這段代碼中doSomethingElse和b應該是doSomething內部私有的,可是卻被暴露出來,這樣會致使以預期以外的形式被使用,產生意料以外的結果,更合理的設計應該是將這些私有的內容隱藏在doSomething內部,例如:作用域

function doSomething(a) {
  function doSomethingElse(a) {
    return a - 1;
  }
  var b;
  b = a + doSomethingElse( a * 2 );
  console.log( b * 3 );
}
doSomething( 2 );

這樣b和doSomethingElse都沒法從doSomething外部訪問,可是這樣也會存在一些問題,首先在全局做用域中聲明瞭doSomething函數,污染了全局做用,其次,必須經過顯示調用才能執行,那麼有沒有什麼辦法既不會污染做用域也不須要調用就能夠自執行呢?答案就是函數表達式,看下面的例子:編譯器

(function doSomething(a) {
  function doSomethingElse(a) {
    return a - 1;
  }
  var b;
  b = a + doSomethingElse( a * 2 );
  console.log( b * 3 );
})(2)

首先來看(function doSomething(){})這是一個函數表達式,和函數聲明不一樣的是用括號包起來的,而後再(function doSomething(a){})()調用傳值,這樣既能自執行也不會污染做用域,社區給這種用法定義了一個術語:IIFE,表明當即執行函數表達式

塊做用域

除JavaScript外不少編程語言都支持塊做用域,儘管你可能寫過很僞塊做用域形式的代碼,最多見的就是for循環:

for(var i=0; i<10; i++) {
  console.log(i)
}

寫這段代碼一般是但願變量i在循環內部使用,可是實際上i會被綁定到外部做用域中,要確保沒有在做用域的其餘地方意外使用i,就只能依靠自覺,這時候塊級做用域就顯得尤其有用,ES6改變了現狀,引入了新的let、const關鍵字,let關鍵字能夠將變量綁定到所在的任意做用域中,也就是let爲其聲明的變量隱式地建立了做用域:

for(let i=0; i<10; i++) {
  console.log(i)        
}
console.log(i) // ReferenceError

這時候i就只會在for循環的內部有效

總結

這篇文章主要介紹了JS做用域相關的內容。若是有錯誤或不嚴謹的地方,歡迎批評指正,若是喜歡,歡迎點贊。

相關文章
相關標籤/搜索