JavaScript系列之做用域和做用域鏈

在上一篇《JavaScript系列之變量對象》中,咱們已經知道一個執行上下文的數據(函數的形參、函數及變量聲明)做爲屬性儲存在變量對象中。javascript

此外,咱們也知道每次進入上下文時都會建立變量對象並填充初始值,而且值會在代碼執行階段進行更新,如今就對執行上下文作更深一步的瞭解。java

先來回顧一下關於執行上下文的三個階段生命週期:git

本章將專門介紹與執行上下文建立階段直接相關的另外一個細節——做用域鏈github

做用域(Scope)

在介紹做用域鏈前,有必要先來了解一下被稱爲做用域(Scope)的特性,那什麼是做用域呢?編程

做用域就是在運行時代碼中不一樣部分中函數和變量的可訪問性。可能這句話並不太好理解,咱們先來看段代碼:數組

function fn() {
    var inVariable = "inner variable";
    console.log(inVariable); // inner variable
}

fn(); 
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined
複製代碼

從上面的代碼中咱們能夠很直觀地體會做用域的概念,變量inVariable在全局做用域沒有聲明,因此在全局做用域下直接取值會報錯。因此咱們能夠這樣理解:做用域就像一個地頭蛇,個人地盤我作主,讓屬於本身域內的變量不會輕易外泄出去。也就是說做用域最大的用處就是隔離變量,不一樣做用域下同名變量不會有衝突。這幾句話總比前面那句好理解多了吧。函數

關於JavaScript 中的做用域類型,ES6 以前 JavaScript 並無塊級做用域,只有全局做用域和函數做用域。ES6的到來,爲咱們提供了‘塊級做用域’,可經過新增命令let和const來體現:ui

  • 全局做用域—變量能夠隨處訪問
  • 函數做用域—變量能夠在定義它們的函數的邊界內訪問
  • 塊級做用域—變量能夠在定義它們的塊中訪問,塊由 { 和 } 分隔

全局做用域和函數做用域

const global = 'global scoped'

function fn() {
    const global = 'function scoped';
    console.log(global); // function scoped
}

fn();
console.log(global); // global scoped
複製代碼

從上面例子能夠看出全局做用域和函數做用域的做用範圍,即便全局變量在函數內部分配了不一樣的值,它也只保留在同一函數的邊界內,互相併不影響,咱們也不會因使用相同的變量名而出錯。再來看個例子加深理解:spa

const global = 'global scoped'
const anotherGlobal = 'also global scoped'

function fn() {
    const global = 'function scoped'
    console.log(global) // function scoped
    const scoped = 'also function scoped'

    function inner() {
        console.log(scoped); // also function scoped
        console.log(anotherGlobal) // also global scoped
    }

    inner();
}

console.log(global); // global scoped
console.log(anotherGlobal); // also global scoped

fn();
inner(); // Uncaught ReferenceError: inner is not defined
複製代碼

在這裏咱們能夠看到 inner() 函數能夠訪問在其父函數中聲明的變量—fn()。每當咱們須要函數內部的變量時,引擎將首先在當前函數做用域內查找它。若是它沒有當前函數做用域內找到它,它將繼續上升,向上一級查找,直到它找到全局做用域內的變量,若是找不到變量,咱們將獲得一個ReferenceError。格外注意函數內層做用域能夠訪問外層做用域的變量,反之則不行調試

除了上面所講的最外層函數外面定義的變量擁有全局做用域,全局做用域還有一種特殊的出現場合:就是全部末聲明直接賦值的變量將自動聲明爲擁有全局做用域的變量

function fn() {
    variable = "undeclared variable";
    var inVariable = "inner variable";
}

fn();
console.log(variable); // undeclared variable
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined
複製代碼

全局做用域有個弊端:若是咱們寫了不少行 JavaScript 代碼,變量定義都沒有用函數包括,那麼它們就所有都在全局做用域中,這樣就會污染全局命名空間,容易引發命名衝突。同時意外的全局變量還會引發內存泄漏,因此在編程時,儘可能避免全局變量的使用,以便後期更快地調試。

還有值得注意的是:塊語句(大括號「{}」中間的語句),如 ifswitch 條件語句或 forwhile 循環語句,不像函數,它們不會建立一個新的做用域。在塊語句中定義的變量將保留在它們已經存在的做用域中。好比:

if (true) {
    // 'if' 條件語句塊不會建立一個新的做用域
    var name = 'miqilin'; // name 依然在全局做用域中
}

console.log(name); // miqilin
複製代碼

JS 的初學者常常須要花點時間才能習慣變量提高,而若是不理解這種特有行爲,就可能致使bug出現 。正由於如此, ES6 引入了塊級做用域,讓變量的生命週期更加可控。

塊級做用域

在ES6中,咱們獲得了兩個新的變量聲明關鍵字 - letconst。它們和var之間的主要區別在於,使用ES6關鍵字聲明的變量是塊做用域,這意味着它們僅在它們定義的代碼塊中可用。塊級做用域在以下狀況被建立:

  1. 在一個函數內部
  2. 在一個代碼塊(由一對花括號包裹)內部

let 聲明的語法與 var 的語法一致。你基本上能夠用 let 來代替 var 進行變量聲明,但會將變量的做用域限制在當前代碼塊中。塊級做用域有如下幾個特色:

  • 聲明變量不會提高到代碼塊頂部

let/const建立的變量不會像使用var聲明的變量那樣被提高到頂部,所以你須要手動將 let/const 聲明放置到頂部,以便讓變量在整個代碼塊內部可用。好比:

cosole.log(name); // Uncaught ReferenceError: cosole is not defined
const name = "miqilin";
複製代碼

因此確保代碼沒有引用錯誤的一種方法是確保只使用letconst進行變量聲明。

  • 禁止重複聲明

若是一個標識符已經在代碼塊內部被定義,那麼在此代碼塊內使用同一個標識符再進行 let 聲明就會拋出錯誤。好比:

var count = 10;
let count = 20; // Uncaught SyntaxError: Identifier 'count' has already been declared
複製代碼

上面例子中count 變量被先後聲明瞭兩次:第一次使用 var ,另外一次使用 let 。由於 let 不能在同一做用域內重複聲明一個已有標識符,此處的 let 聲明就會拋出錯誤。但若是在嵌套的做用域內使用 let 聲明一個同名的新變量,則不會拋出錯誤:

var count = 10;
// 不會拋出錯誤
if (condition) {
let count = 20;
// 其餘代碼
}
複製代碼
  • 循環中的綁定塊做用域的妙用

開發者可能最但願實現for循環的塊級做用域了,由於能夠把聲明的計數器變量限制在循環內,好比:

for (let i = 0; i < 10; i++) {
  // ...
}
console.log(i);
// ReferenceError: i is not defined
複製代碼

上面代碼中,由於用let聲明計數器i,只在for循環體內有效,因此在循環體外引用就會報錯。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10
複製代碼

上面代碼中,變量ivar命令聲明的,在全局範圍內都有效,因此全局只有一個變量i。每一次循環,變量i的值都會發生改變,而循環內被賦給數組a的函數內部的console.log(i),裏面的i指向的就是全局的i。也就是說,全部數組a的成員裏面的i,指向的都是同一個i,致使運行時輸出的是最後一輪的i的值,也就是 10。

若是換使用let,聲明的變量僅在塊級做用域內有效,最後輸出的是 6。

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6
複製代碼

上面代碼中,變量ilet聲明的,當前的i只在本輪循環有效,因此每一次循環的i其實都是一個新的變量,因此最後輸出的是6。你可能會問,若是每一輪循環的變量i都是從新聲明的,那它怎麼知道上一輪循環的值,從而計算出本輪循環的值?這是由於 JavaScript 引擎內部會記住上一輪循環的值,初始化本輪的變量i時,就在上一輪循環的基礎上進行計算。

另外,for循環還有一個特別之處,就是設置循環變量的那部分是一個父做用域,而循環體內部是一個單獨的子做用域。

for (let i = 0; i < 5; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc
// abc
// abc
複製代碼

上面代碼正確運行,輸出了 5 次abc。這代表函數內部的變量i與循環變量i不在同一個做用域,有各自單獨的做用域。

做用域鏈(Scope Chain)

上面用一大篇幅來說解做用域,其實在裏面就有涉及到做用域鏈的知識了。簡單的來講,當查找變量的時候,會先從當前上下文的變量對象中查找,若是沒有找到,就會從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執行上下文的變量對象構成的鏈表就叫作做用域鏈。看下面一個例子:

function a() {
    
    function b() {
        console.log(myVar);
    }

    var myVar = 2;
    b();
}

var myVar = 1;
a(); // 2
b(); // Uncaught ReferenceError: b is not defined
複製代碼

最後加以執行a()b(),這時候咱們會發現兩件事:

1.執行a()會獲得2的結果:之因此會有這樣的結果,是由於當咱們執行function a裏面的function b時,由於在function b裏面它找不到myVar這個變量,所以它開始往它的外層去搜尋,而這時候它的父級做用域是function a,在function a裏面它便找到了myVar = 2,所以它就再也不往外部環境 (myVar = 1)去找了,直接返回了2這樣的結果。

2.b()會獲得b is not defined的結果:之因此b會是not defined(記得是not defined不是undefined哦!),是由於這時候在最外層的全局上下文(global execution context)中,找不到function b

而從b() --> a() --> global execution context這樣的鏈,就稱爲做用域鏈(Scope Chain)

若是咱們把function a裏面對於myVar的聲明拿掉的話,它纔會繼續往外層搜尋myVar,直到找到全局做用域中的聲明myVar = 1,這時候纔會返回1的結果。

function a() {
    
    function b() {
        console.log(myVar);
    }

    //var myVar = 2;
    b();
}

var myVar = 1;
a(); // 1
複製代碼

若是咱們更進一步的把全局做用域中,對於myVar的聲明也拿掉,那麼如今在全局做用域中也找不到myVar這個變量了,也就是說,在這整個做用域鏈中都找不到myVar,所以可想而知,最後的結果是not defined

function a() {
    
    function b() {
        console.log(myVar);
    }

    //var myVar = 2;
    b();
}

//var myVar = 1;
a(); // Uncaught ReferenceError: myVar is not defined
複製代碼

若是以爲文章對你有些許幫助,歡迎在個人GitHub博客點贊和關注,感激涕零!

相關文章
相關標籤/搜索