JavaScript漫談之深刻理解做用域與閉包

若有問題,歡迎指教。更多內容請關注 GitHub

1、做用域

做用域是追蹤全部變量的方式,是代碼的當前上下文以及對變量的訪問權限。瞭解做用域,能夠知道變量/函數在何處可訪問。javascript

JavaScript使用詞法做用域,這種方法容許做用域嵌套,所以外部做用域包含內部做用域。css

一、全局做用域

若是一個變量在全部函數或花括號({})以外聲明,則它是在全局做用域內定義的前端

全局變量能夠在代碼的任何地方使用。java

const name = 'sueRimn';

function person () {
    console.log(name);
}

console.log(name); // 'sueRimn'
person() // 'sueRimn'

雖然能夠在全局範圍內聲明變量,但不建議這樣作,由於存在命名衝突的可能性。git

若是使用constlet聲明變量,那麼每當發生名稱衝突時,都會拋錯。這是不可取的。github

let name = 'sueRimn';
let name = '八至'; // 報錯

若是使用var聲明變量,第二個變量會在聲明後覆蓋第一個變量。這也不可取,由於代碼將很難調試。web

var name = 'sueRimn';
var name = '八至';
console.log(name); // '八至'

因此,你應該聲明局部變量,而不是全局變量。瀏覽器

這隻適用於web瀏覽器中的JavaScript。

二、局部做用域

只在代碼的特定部分中可用的變量被認爲是在局部做用域中。這些變量也稱爲局部變量。閉包

JavaScript中,有兩種局部做用域:函數做用域和塊做用域函數

(1)塊做用域

當在一個大括號({})內聲明一個constlet變量時,只能在那個大括號內訪問這個變量。

{
    let name = 'sueRimn';
    console.log(name); // 'sueRimn'
}

console.log(name); // error, name is not defined

塊做用域是函數做用域的一個子集,由於函數須要用花括號聲明(除非使用帶隱式返回的箭頭函數)。

(2)函數做用域

在函數中聲明變量時,只能在函數中訪問該變量,對變量的訪問僅限於函數的局部做用域。

function person () {
    let name = 'sueRimn';
    console.log(name); 
}

person(); // 'sueRimn'
console.log(name); // 報錯 name is not defined

a)函數提高與做用域

當使用函數聲明聲明函數時,老是將其提高到當前範圍的頂部,如下兩種結果是同樣的:

person(); // 'sueRimn is beautiful'

function person () {
    console.log('sueRimn is beautiful');
}

person(); // 'sueRimn is beautiful'

當使用函數表達式代表時,函數不會提高到當前範圍的頂部。

person(); // 報錯 person is not defined
const person = () =>{
    console.log('sueRimn is beautiful');
}

person(); // 'sueRimn is beautiful'

因此,儘可能在使用函數以前聲明它。

b)獨立函數不能訪問彼此的做用域

若是分別獨立聲明函數,即便函數之間能夠彼此調用,可是沒法訪問彼此的變量,由於每一個函數的做用域是獨立的。

function name () {
    const name = 'sueRimn';
}

function age () {
    const age = '22'
    name()
    console.log(name); // error name id not defined.
}

c)嵌套做用域

當在一個函數中定義另外一個函數時,內部函數能夠訪問外部函數的做用域。函數嵌套也會致使做用域嵌套,做用域嵌套也稱爲詞法做用域閉包,也成爲靜態做用域

可是,外部函數沒法訪問內部函數的做用域。就像單向玻璃,你在裏面能夠看見外面,外面的看不見裏面。

function person () {
    let name = 'sueRimn';
    function my () {
        console.log('my name is' + name);
    }
    console.log(name);
    my();
}

// 打印結果是:
'sueRimn' 
'my name is sueRimn'

三、做用域鏈

(1)做用域與執行上下文

JavaScript屬於解釋型語言,JavaScript的執行分爲解釋和執行兩個階段:

解釋階段:

  • 詞法分析
  • 語法分析
  • 做用域規則肯定

執行階段:

  • 建立執行上下文
  • 執行函數代碼
  • 垃圾回收

靜態做用域是指函數定義決定了函數的做用域。JavaScript採用的是靜態做用域。JavaScript解釋階段便會肯定做用域規則,所以做用域在函數定義時就已經肯定了,而不是在函數調用時肯定。

執行上下文是函數執行以前建立的,即在函數執行準備階段建立好的。

執行上下文最明顯的就是this的指向是執行時肯定的,即函數調用決定執行上下文的指向。

(2)深刻理解做用域鏈

a)定義

由於 JavaScript 採用的是詞法做用域(靜態做用域),函數定義時肯定本身的做用域做爲該函數的屬性,做用域沒法改變,一直保存至函數銷燬。

因此說函數定義時是基於靜態做用域的,由於即便函數不調用,其[[scope]]屬性也會一直存在,而且保持不變。

每一個上下文都有本身的變量對象,對於全局上下文,它是全局對象自身;對於函數,它是活動對象。

當查找變量對象時,計算機會從當前上下文的變量對象中找,若是找不到,就會從父級上下文也就是層層往上查找,直到全局上下文,到那時還找不到,就會拋出ReferenceError

做用域鏈正是內部上下文全部變量對象的鏈表,用於變量查詢。

函數上下文的做用域鏈在函數調用時建立的,包含活動對象和這個函數內部的[[scope]]屬性。

由於當函數調用時,會生成執行上下文,此執行上下文的[[scope]]和定義函數時的[[scope]]是不一樣的,執行上下文的[[scope]]是在函數定義時的[[scope]]屬性基礎上又新增一個當前AO對象構成的。

所以,函數定義時候的[[scope]]做爲函數的屬性,函數執行時候的[[scope]]做爲函數執行上下文的屬性。

通常狀況下,一個做用域鏈包括父級變量對象(variable object)(做用域鏈的頂部)、函數自身變量VO和活動對象(activation object)。

當查找標識符的時候,會從做用域鏈的活動對象部分開始查找,而後(若是標識符沒有在活動對象中找到)查找做用域鏈的頂部,循環往復,就像做用域鏈那樣。

標識符解析過程與函數聲明週期相關。

b)瞭解函數的聲明週期

函數週期分爲函數建立和函數調用

函數建立

在進入上下文時函數聲明放到變量/活動(VO/AO)對象中。

函數調用

進入上下文建立AO/VO以後,上下文的Scope屬性(變量查找的一個做用域鏈)做以下定義:

Scope = AO|VO + [[Scope]]

一個函數對象被調用的時候,會建立一個活動對象(也就是一個對象),對於每個函數的形參,都命名爲該活動對象的命名屬性,而後將這個活動對象做爲此時的做用域鏈最前端,並將這個函數對象的[[scope]]加入到做用域鏈中。

2、閉包

一、定義

閉包與詞法做用域直接相關,函數建立時存儲做用域,直到到函數銷燬都不會改變。

實際上,閉包是由函數以及建立該函數的詞法環境組合而成。這個環境包含了這個閉包建立時所能訪問的全部局部變量

閉包容許你從內部函數訪問外部函數的做用域。在JavaScript中,每次在函數調用時都會建立閉包。

  • 閉包是嵌套函數,能夠訪問外部範圍
  • 返回外部函數後,經過對內部函數(閉包)的引用,能夠防止破壞外部做用域
  • 若是外部做用域的變量被更改,它將影響後續調用

二、使用閉包

要使用閉包,就要在一個函數中定義另外一個函數並暴露該內部函數。若要公開一個內部函數,就要將其返回或傳遞給另外一個函數。即便被外部函數返回以後,內部函數也能夠訪問外部函數做用域中的變量。

(1)使用閉包保護私有數據

在JavaScript中,閉包是用來保護數據隱私的主要機制。閉包是外部範圍和程序其他部分之間的通道。它能夠選擇公開什麼數據,而不公開什麼數據。

function person() {
    let age = 22; 
    return { 
        getAge: function() {
            return age;
        },
        setAge: function(v) {
            age = v;
        }
    };
}

obj = person();

console.log(obj.getAge()); // 22

obj.setAge(22);
console.log(obj.getAge()); // 22

obj.setAge("sueRimn");
console.log(obj.getAge()); // sueRimn

這裏函數返回了一個有兩個函數的對象。由於它們是綁定到局部做用域的對象的屬性,因此它們是閉包。經過getAgesetAge,能夠操做age屬性,但不能直接訪問它。

對象不是產生數據隱私的惟一方法。閉包也能夠用來建立有狀態函數,這些函數的返回值可能會受到其內部狀態的影響,好比:

const name = name => () => name;

(2)使用閉包建立迭代器

因爲保存了來自外部做用域的數據,因此使用閉包建立迭代器至關容易。

function buildContor(i) { 
    var contor = i;
    var displayContor = function() {
        console.log(contor++);
        contor++;
    };
    return displayContor; 
}

var myContor = buildContor(1);
myContor(); // 1
myContor(); // 2
myContor(); // 3

// new closure - new outer scope - new contor variable
var myOtherContor = buildContor(10);
myOtherContor(); // 10 
myOtherContor(); // 11

// myContor was not affected 
myContor(); // 4

上面的buildContor()函數其實是一個迭代器,每次調用都建立一個新的迭代器,並使用固定的起始索引,而後在每次連續調用迭代器時,返回下一個值。

每次調用其中一個計數器時,經過改變這個變量的值,會改變這個閉包的詞法環境。然而在一個閉包內對變量的修改,不會影響到另一個閉包中的變量。

(3)使用jQuery時的閉包

jQuery(或任何JavaScript)中的事件都是閉包。事件處理程序能夠訪問外部做用域。

$(function() { 
    var contor = 0;
    $("#Button").click(function() { // 閉包從外部做用域更新變量
        contor++; 
    }
}

(4)使用閉包在JavaScript中實現單例

單例對象是在程序執行過程當中只有一個實例的對象。

咱們知道,每次函數調用都會建立一個新的閉包。但若是咱們想阻止外部函數的另外一次調用呢?

很簡單:使用匿名函數。

var person = function () {
    var age = 22;
    return {
        get: function () {
            return "age: " + age;
        },
        increment: function() {
            age++;
        }
    };
}();  // 注意 單例是該函數回調的結果

console.log(person.get()); // age:22
console.log(person.get()); // age:22

person.increment();
console.log(person.get()); // age:23
person.increment();
console.log(person.get()); // age: 24

這個例子與前面惟一的區別是外部函數是匿名的,它沒有名字。

咱們聲明它並當即調用它,person對象(即閉包)是訪問其做用域的惟一來源。對於確保建立的age不會有多個做用域是很是有用的。

三、性能考量

若是不是某些特定任務須要使用閉包,在其它函數中建立函數是不明智的,由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。解決方法是,在退出函數以前,將不使用的局部變量所有刪除。

3、小結

做用域和閉包若是從單向玻璃理解就很容易。

做用域是在函數定義時產生的,在一個函數內定義任何內部函數,其內部函數稱爲閉包,閉包保留對外部函數中建立的變量的訪問權。

參考:

Master the JavaScript Interview: What is a Closure?

Closures in Javascript for beginners

JavaScript Scope and Closures

Closures
深刻理解JavaScript做用域和做用域鏈

相關文章
相關標籤/搜索