前端面試必會 | 一文讀懂 JavaScript 中的做用域和做用域鏈

本文翻譯自 blog.bitsrc.io/understandi…,做者 Sukhjinder Arora,翻譯時有部分刪改,標題有修改。javascript

做用域和做用域鏈是 JavaScript 和不少編程語言的基本概念。這些概念會讓不少 JavaScript 開發者感到困惑,可是若是想掌握 JavaScript 它們又是必不可少的。前端

正確理解這些概念將有助於您編寫更好,更有效和更乾淨的代碼。反過來,它將幫助您成爲更好的JavaScript開發人員。java

所以,在本文中,我將解釋什麼是做用域和做用域鏈,以及 JavaScript 引擎如何進行變量查找和這些概念的內部原理。編程

什麼是做用域

JavaScript 中的做用域是指變量的可訪問性或可見性。也就是說,程序的哪些部分能夠訪問該變量,或者該變量在何處可見。數組

做用域爲何重要?

  1. 做用域的主要好處是安全性。也就是說,只能從程序的特定區域訪問變量。使用做用域,咱們能夠避免程序其餘部分對變量的意外修改。
  2. 做用域還減小了命名衝突。也就是說,咱們能夠在不一樣的範圍內使用相同的變量名。

做用域類型

JavaScript 中有三種類型的做用域:瀏覽器

  1. 全局做用域;
  2. 函數做用域;
  3. 塊做用域;

1. 全局做用域(Global Scope)

不在任何函數或塊(一對花括號)內的任何變量都在全局做用域內。能夠從程序的任何位置訪問全局做用域內的變量。例如:安全

var greeting = 'Hello World!';
function greet() {
  console.log(greeting);
}
// Prints 'Hello World!'
greet();
複製代碼

2. 局部做用域或者函數做用域

在函數內部聲明的變量在局部做用域內。它們只能從該函數內部訪問,這意味着它們不能從外部代碼訪問。例如:編程語言

function greet() {
  var greeting = 'Hello World!';
  console.log(greeting);
}
// Prints 'Hello World!'
greet();
// Uncaught ReferenceError: greeting is not defined
console.log(greeting);
複製代碼

3. 塊級做用域

ES6 引入了 letconst 變量,與 var 變量不一樣,它們的做用域能夠是最接近的花括號對。這意味着,不能從那對花括號以外訪問它們。例如:函數

{
  let greeting = 'Hello World!';
  var lang = 'English';
  console.log(greeting); // Prints 'Hello World!'
}
// Prints 'English'
console.log(lang);
// Uncaught ReferenceError: greeting is not defined
console.log(greeting);
複製代碼

做用域嵌套

就像 JavaScript 中的函數同樣,一個做用域能夠嵌套在另外一個做用域內。例如:佈局

var name = 'Peter';
function greet() {
  var greeting = 'Hello';
  {
    let lang = 'English';
    console.log(`${lang}: ${greeting} ${name}`);
  }
}
greet();
複製代碼

在這裏,咱們有 3 個做用域相互嵌套。首先,塊做用域(因爲 let 變量而建立)嵌套在局部做用域或函數做用域內,然後者又嵌套在全局做用域內。

詞法做用域

詞法做用域(也稱爲靜態做用域)從字面上講是指做用域是在詞法分析時(一般稱爲編譯)而非運行時肯定的。例如:

let number = 42;
function printNumber() {
  console.log(number);
}
function log() {
  let number = 54;
  printNumber();
}
// Prints 42
log();
複製代碼

在這裏,console.log(number) 老是會打印 42 不管 printNumber() 在何處被調用。這與動態做用域的語言不一樣,動態做用域語言中 printNumber() 在不一樣的位置執行將會打印不一樣的值。

若是上面的代碼是用支持動態做用域的語言編寫的,console.log(number) 則會打印出來 54

使用詞法做用域,咱們能夠僅經過查看源代碼來肯定變量的範圍。而使用動態做用域,只有在執行代碼後才能肯定範圍。

大多數編程語言都支持詞法或靜態做用域,例如 C,C++,Java,JavaScript。Perl 支持靜態和動態做用域。

做用域鏈

在 JavaScript 中使用變量時,JavaScript 引擎將嘗試在當前做用域中查找變量的值。若是找不到變量,它將查找外部做用域並繼續這樣作,直到找到變量或到達全局做用域爲止。

若是仍然找不到變量,它將在全局做用域內隱式聲明變量(若是不是在嚴格模式下)或返回錯誤。

例如:

let foo = 'foo';
function bar() {
  let baz = 'baz';
  // Prints 'baz'
  console.log(baz);
  // Prints 'foo'
  console.log(foo);
  number = 42;
  console.log(number);  // Prints 42
}
bar();
複製代碼

執行 bar() 時,JavaScript 引擎將查找 baz 變量並在當前做用域中找到它。接下來,JavaScript 引擎會在當前做用域中查找 foo 變量,但沒法在當前做用域中找到,因此引擎會在外層做用域中查找並找到這個變量。

以後咱們給 number 變量賦值 42,JavaScript 引擎會先在當前做用域查找而後在外層做用域繼續查找。

若是是在非嚴格模式下執行代碼,引擎將會建立一個新變量 number,並給它賦值 42。若是運行在嚴格模式中將會報錯。

嚴格模式下報錯

所以,當使用變量時,引擎將遍歷做用域鏈,直到找到該變量爲止。

做用域和做用域鏈是如何工做的?

到目前爲止,咱們已經討論了什麼是做用域和做用域的類型。接下來咱們看看 JavaScript 引擎是如何定義變量的做用域的以及它是如何進行變量查找的。

爲了瞭解 JavaScript 引擎如何執行變量查找,咱們必須瞭解 JavaScript 中的詞法環境的概念。

詞法環境是什麼?

詞法環境是用來保存標識符和變量映射關係的地方。標識符是變量或者函數的名字,變量是對實際對象(包括函數對象和數組對象)或者原始值的引用。

簡而言之,詞法環境是存儲變量和對象引用的地方。

注意—不要把詞法做用域詞法環境混淆了。詞法做用域是在編譯時肯定的做用域,而詞法環境是在程序執行過程當中存儲變量的地方

從概念上講,詞法環境以下所示:

lexicalEnvironment = {
  a: 25,
  obj: <ref. to the object> } 複製代碼

看成用域內的代碼執行的時候一個新的詞法環境纔會被建立。詞法環境也有一個指向外部詞法環境的引用 outer(外層做用域)。例如:

lexicalEnvironment = {
  a: 25,
  obj: <ref. to the object>
  outer: <outer lexical environemt>
}
複製代碼

JavaScript 引擎如何查找變量?

如今咱們知道了做用域,做用域鏈和詞法環境。接下來咱們看看 JavaScript 引擎如何使用詞法環境來肯定做用域和做用域鏈。

讓咱們看一下下面的代碼片斷以瞭解以上概念。

let greeting = 'Hello';
function greet() {
  let name = 'Peter';
  console.log(greeting + ' ' + name);
}
greet();
{
  let greeting = 'Hello World!'
  console.log(greeting);
}
複製代碼

加載上述腳本後,將建立一個全局詞法環境,其中包含在全局做用域內定義的變量和函數。例如:

globalLexicalEnvironment = {
  greeting: 'Hello'
  greet: <ref. to greet function>
  outer: <null>
}
複製代碼

在這裏,外部詞法環境被設置爲 null ,由於全局做用域沒有外部做用域。

以後將會執行 greet()。因此將會爲 greet() 建立一個新的詞法環境。以下:

functionLexicalEnvironment = {
  name: 'Peter'
  outer: <globalLexicalEnvironment> } 複製代碼

這裏把外部詞法環境設置爲 globalLexicalEnvironment,由於它的外部做用域是全局做用域。

以後,JavaScript 引擎將會執行 console.log(greeting + ' ' + name)

JavaScript 引擎嘗試在函數的詞法環境中查找 greetingname 變量,它能夠在當前詞法環境中找到 name,可是找不到 greeting

因此它在 greet 函數的外層詞法環境(全局詞法環境)中查找並找到了 greeting 變量。

接下來 JavaScript 引擎執行代碼塊內部的代碼,引擎給代碼塊建立了一個新的詞法環境。以下:

blockLexicalEnvironment = {
  greeting: 'Hello World',
  outer: <globalLexicalEnvironment> } 複製代碼

接下來,執行 console.log(greeting) 語句,JavaScript 引擎在當前詞法環境中找到 greeting 變量並使用該變量。所以,它不會在變量的外部詞法環境(全局詞法環境)中查找。

注意— JavaScript 引擎只會爲 let const 聲明的變量建立詞法環境,不會爲 var 聲明的變量建立。 var 聲明的變量會被添加到當前的詞法環境(全局或者函數詞法環境中)而不是塊級詞法環境中。

所以,當在程序中使用變量時,JavaScript 引擎將嘗試在當前詞法環境中查找該變量,若是沒法在該詞法環境中找到該變量,它將在外部詞法環境中查找該變量。這就是 JavaScript 引擎執行變量查找的方式。

總結

簡而言之,做用域是一個可見和可訪問變量的區域。就像函數同樣,JavaScript 中的做用域能夠嵌套,而且 JavaScript 引擎遍歷做用域鏈以查找程序中使用的變量。

JavaScript 引擎使用詞法做用域,這意味着變量的做用域在編譯時肯定。JavaScript 引擎使用詞法環境在程序執行期間存儲變量。

做用域和做用域鏈是每一個 JavaScript 開發人員都應理解的 JavaScript 基本概念。熟悉這些概念將幫助您成爲一個更有效率、更優秀的 JavaScript 開發人員。

最後

往期精彩:

關注公衆號能夠看更多哦。

感謝閱讀,歡迎關注個人公衆號 雲影 sky,帶你解讀前端技術,掌握最本質的技能。關注公衆號能夠拉你進討論羣,有任何問題都會回覆。

公衆號
相關文章
相關標籤/搜索