JS進擊之路:作用域

william_li發表於2019-03-10

引言

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

編譯原理

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

  • 分詞/詞法分析:將程式碼語句分解成有意義的程式碼塊,又叫詞法單元。
  • 解析/語法分析:將詞法單元轉換一個逐級巢狀的具有語法規則的樹狀結構,又叫抽象語法樹(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來說查詢過程一樣。作用域查詢始終從執行時最內層開始查詢,逐級向外查詢,直到遇見第一個匹配的變數為止。

函式作用域

函式作用域指的是屬於這個函式的全部變數都可以在整個函式的範圍內使用及複用,這是大家都知道的定義,但是函式作用域的存在到底有什麼用呢?接下來就一起看看函式作用域的秒用。

隱藏內部實現

隱藏內部實現就是將變數和函式包裹在一個函式的作用域中,達到隱藏的目的,為什麼要這麼做呢?軟體設計中有一個非常有名的原則叫最小暴露原則,指最小限度暴露必要內容,而將其他內容都隱藏起來,比如模組或物件的API設計。用函式作用域來包裹變數和函式來達到最小暴露原則,阻止外部直接訪問,來看下面的例子:

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作用域相關的內容。如果有錯誤或不嚴謹的地方,歡迎批評指正,如果喜歡,歡迎點贊。

相關文章