理解Javascript的做用域和做用域鏈

前言

本文2771字,閱讀大約須要8分鐘。

總括: 本文講解了Javascript的做用域,做用域類型,做用域鏈等概念以及Javascript是如何去創建做用域鏈並尋找變量的。javascript

一花凋零,荒蕪不了整個春天。前端

正文

做用域和做用域鏈在Javascript和不少其它的編程語言中都是一種基礎概念。但不少Javascript開發者並不真正理解它們,但這些概念對掌握Javascript相當重要。java

正確的去理解這個概念有利於你去寫更好,更高效和更簡潔的代碼,讓你成爲一個更優秀的Javascript開發者。編程

所以,在本文中,我將會向你們解釋清楚什麼是做用域和做用域鏈,以及Javascript引擎在內部是如何經過它們操做和查找變量的。數組

事不宜遲,正文開始 :)安全

什麼是做用域

Javascript中的做用域說的是變量的可訪問性和可見性。也就是說整個程序中哪些部分能夠訪問這個變量,或者說這個變量都在哪些地方可見。編程語言

爲何做用域很重要

  1. 做用域最爲重要的一點是安全。變量只能在特定的區域內才能被訪問,有了做用域咱們就能夠避免在程序其它位置意外對某個變量作出修改。
  2. 做用域也會減輕命名的壓力。咱們能夠在不一樣的做用域下面定義相同的變量名。

做用域的類型

Javascript中有三種做用域:函數

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

1. 全局做用域

任何不在函數中或是大括號中聲明的變量,都是在全局做用域下,全局做用域下聲明的變量能夠在程序的任意位置訪問。例如:學習

// 全局變量
var greeting = 'Hello World!';
function greet() {
  console.log(greeting);
}
// 打印 'Hello World!'
greet();

2. 函數做用域

函數做用域也叫局部做用域,若是一個變量是在函數內部聲明的它就在一個函數做用域下面。這些變量只能在函數內部訪問,不能在函數之外去訪問。例如:spa

function greet() {
  var greeting = 'Hello World!';
  console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 報錯: Uncaught ReferenceError: greeting is not defined
console.log(greeting);

3. 塊級做用域

ES6引入了letconst關鍵字,和var關鍵字不一樣,在大括號中使用letconst聲明的變量存在於塊級做用域中。在大括號以外不能訪問這些變量。看例子:

{
  // 塊級做用域中的變量
  let greeting = 'Hello World!';
  var lang = 'English';
  console.log(greeting); // Prints 'Hello World!'
}
// 變量 'English'
console.log(lang);
// 報錯:Uncaught ReferenceError: greeting is not defined
console.log(greeting);

上面代碼中能夠看出,在大括號內使用var聲明的變量lang是能夠在大括號以外訪問的。使用var聲明的變量不存在塊級做用域中。

做用域嵌套

像Javascript中函數能夠在一個函數內部聲明另外一個函數同樣,做用域也能夠嵌套在另外一個做用域中。請看例子:

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

這裏咱們有三層做用域嵌套,首先第一層是一個塊級做用域(let聲明的),被嵌套在一個函數做用域(greet函數)中,最外層做用域是全局做用域。

詞法做用域

詞法做用域(也叫靜態做用域)從字面意義上看是說做用域在詞法化階段(一般是編譯階段)肯定而非執行階段肯定的。看例子:

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

上面代碼能夠看出不管printNumber()在哪裏調用console.log(number)都會打印42動態做用域不一樣,console.log(number)這行代碼打印什麼取決於函數printNumber()在哪裏調用。

若是是動態做用域,上面console.log(number)這行代碼就會打印54

使用詞法做用域,咱們能夠僅僅看源代碼就能夠肯定一個變量的做用範圍,但若是是動態做用域,代碼執行以前咱們無法肯定變量的做用範圍。

像C,C++,Java,Javascript等大多數編程語言都支持靜態做用域。Perl 既支持動態做用域也支持靜態做用域。

做用域鏈

當在Javascript中使用一個變量的時候,首先Javascript引擎會嘗試在當前做用域下去尋找該變量,若是沒找到,再到它的上層做用域尋找,以此類推直到找到該變量或是已經到了全局做用域。

若是在全局做用域裏仍然找不到該變量,它就會在全局範圍內隱式聲明該變量(非嚴格模式下)或是直接報錯。

例如:

let foo = 'foo';
function bar() {
  let baz = 'baz';
  // 打印 'baz'
  console.log(baz);
  // 打印 'foo'
  console.log(foo);
  number = 42;
  console.log(number);  // 打印 42
}
bar();

當函數bar()被調用,Javascript引擎首先在當前做用域下尋找變量baz,而後尋找foo變量但發如今當前做用域下找不到,而後繼續在外部做用域尋找找到了它(這裏是在全局做用域找到的)。

而後將42賦值給變量number。Javascript引擎會在當前做用域以及外部做用域下一步步尋找number變量(沒找到)。

若是是在非嚴格模式下,引擎會建立一個number的全局變量並把42賦值給它。但若是是嚴格模式下就會報錯了。

結論:當使用一個變量的時候,Javascript引擎會循着做用域鏈一層一層往上找該變量,直到找到該變量爲止。

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

以上內容已經講解了做用域,做用域的類型,如今讓咱們看下Javascript引擎是如何肯定變量的做用域鏈和如何去查找變量的。

要想理解Javascript是如何進行變量查找的,必需要了解Javascript中詞法環境這個概念(請參考:理解Javascript中的執行上下文和執行棧)。

什麼是詞法環境

所謂詞法環境就是一種標識符—變量映射的結構(這裏的標識符指的是變量/函數的名字,變量是對實際對象[包含函數和數組類型的對象]或基礎數據類型的引用)。

簡單地說,詞法環境是Javascript引擎用來存儲變量和對象引用的地方。

注意:不要混淆了詞法環境和詞法做用域,詞法做用域是在代碼編譯階段肯定的做用域(譯者注:一個抽象的概念),而詞法環境是Javascript引擎用來存儲變量和對象引用的地方(譯者注:一個具象的概念)。

一個詞法環境就像下面這樣:

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

只有當該做用域的代碼被執行的時候,引擎纔會爲那個做用域建立一個新的詞法環境。詞法環境還會記錄所引用的外部詞法環境(即外部做用域)。例:

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}`); // Hello Peter
}
greet();
{
  let greeting = 'Hello World!'
  console.log(greeting); // Hello World!
}

上述代碼加載後,首先會建立一個全局詞法環境,其中包含在全局範圍內聲明的變量和函數。像下面這樣:

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

這裏的outer字段(也就是外部詞法環境)被設置爲了null,是由於全局詞法環境已是最頂層的詞法環境了。

而後,咱們調用了greet()函數,而後一個新的詞法環境會被被建立:

functionLexicalEnvironment = {
  name: 'Peter'
  outer: <globalLexicalEnvironment>
}

這裏的outer字段被設置爲了globalLexicalEnvironment,是由於他的外部做用域就是全局做用域。

而後,執行console.log(`&dollar;{greeting} ${name}`)這行代碼,Javascript引擎首先在當前函數的詞法環境中尋找變量greetingname,但只找到了name,沒找到greeting。而後繼續在上層的詞法環境中找greeting(這裏是全局做詞法環境)。最後在全局詞法環境中找到了greeting

緊接着執行那段在大括號裏的代碼,爲這個塊級建立一個新的詞法環境。以下:

blockLexicalEnvironment = {
  greeting: 'Hello World',
  outer: <globalLexicalEnvironment>
}

而後執行console.log(greeting)這行代碼,首先在本層詞法環境中找greeting,OK,找到,結束。此時就不會再去外部做用域(這裏是全局做用域)尋找該變量了。

注意:只有letconst聲明變量纔會建立一個新的詞法環境存儲,使用var聲明的變量會被存儲在當前塊(大括號)所在的詞法環境中(全局詞法環境或是函數詞法環境中)。

結論:當一個變量被使用時,Javascript引擎會首先在當前的詞法環境中進行尋找,若是找不到就找上層詞法環境中尋找,直到找到爲止。

結論

做用域就是一個變量可訪問和可見的區域,和函數同樣,Javascript的做用域也能夠嵌套,Javascript引擎會層層遍歷做用域來尋找用到的變量。

Javascript使用詞法做用域,這意味着變量的做用在編譯階段就會被肯定。

Javascript引擎在程序執行期間使用詞法環境來存儲變量和函數。

做用域和做用域鏈是Javascript中的基礎概念,理解做用域和做用域鏈能讓你成爲一個更優秀的Javascript開發者。

以上。


能力有限,水平通常,歡迎勘誤,不勝感激。

訂閱更多文章可關注公衆號「前端進階學習」,回覆「666」,獲取一攬子前端技術書籍

前端進階學習

相關文章
相關標籤/搜索