javascript中關於做用域和閉包

  1. 列表項目javascript

前言

學習了javascript已經好久了,關於這個語言中的這兩個特性也是早已耳熟能詳,可是在實際的使用的過程當中或者是遇到相關的問題的時候,仍是不能很好的解決。
所以我以爲頗有必要深刻的學習而且記錄這個問題,以便在從此的學習和使用的過程當中回顧。html

正文

1. 全局做用域

  • 瀏覽器環境java

    全部瀏覽器都支持 window 對象,它表示瀏覽器窗口,JavaScript 全局對象、函數以及變量均自動成爲 window 對象的成員。

    因此,全局變量是 window 對象的屬性,全局函數是 window 對象的方法,甚至 HTML DOM 的 document 也是 window 對象的屬性之一。

    全局變量是JavaScript裏生命週期(一個變量多長時間內保持必定的值)最長的變量,其將跨越整個程序,能夠被程序中的任何函數方法訪問。

    在全局下聲明的變量都會在window對象下,都在全局做用域中,咱們能夠經過window對象訪問,也能夠直接訪問。程序員

  • Node環境編程

    全局對象是global對象,與window相似,在全局下聲明的全部的變量都在global對象之下,都在全局做用域中,能夠經過glocal訪問,也能夠經過變量名訪問。數組

    var name = 'clam';
    console.log(name); // clam
    // 瀏覽器環境
    console.log(window.name); // clam
    // Node環境
    console.log(global.name); // clam
  • 聲明方式產生全局變量瀏覽器

    在js的任何位置,聲明變量的時候沒有使用var關鍵字,這個變量就是全局變量。閉包

    function add(a,b){
        return sum = a+b;
    }
    
    add(1,2); // 3
    console.log(sum); //3
    
    /* 至關於如下代碼
*/
var sum;
function add(a,b){
    return sum = a+b;
}
add(1,2) ; // 3
console.log(sum);
```

全局變量存在於整個函數的生命週期中,然而其在全局範圍內很容易被篡改,咱們在使用全局變量時必定要當心,儘可能不要使用全局變量。在函數內部聲明變量沒有使用var也會產生全局變量,會爲咱們形成一些混亂,好比變量覆蓋等。因此,咱們在聲明變量的任什麼時候候最好都要帶上var。app

全局變量存在於程序的整個生命週期,但並非經過其引用咱們必定能夠訪問到全局變量。編程語言

2. 詞法做用域

詞法做用域:函數在定義它們的做用域裏運行,而不是在執行它們的做用域裏運行。也就是說詞法做用域取決於源碼,經過靜態分析就能肯定,所以詞法做用域也叫作靜態做用域。with和eval除外,因此只能說JS的做用域機制很是接近詞法做用域(Lexical scope)。詞法做用域也能夠理解爲一個變量的可見性,及其文本表述的模擬值。

var name = "global";

function fun() {
    var name = "clam";
    return name;
}
console.log(fun()); // 輸出:clam
console.log(name); // 輸出:global

在一般狀況下,變量的查詢從最近接的綁定上下文開始,向外部逐漸擴展,直到查詢到第一個綁定,一旦完成查找就結束搜索。就像上例,先查找離它最近的name="clam",查詢完成後就結束了,將第一個獲取的值做爲變量的值。

3. 動態做用域

動態做用域與詞法做用域相對而言的,不一樣於詞法做用域在定義時肯定,動態做用域在執行時肯定,其生存週期到代碼片斷執行爲止。動態變量存在於動態做用域中,任何給定的綁定的值,在肯定調用其函數以前,都是不可知的。

在代碼執行時,對應的做用域鏈經常是保持靜態的。然而當遇到with語句、call方法、apply方法和try-catch中的catch時,會改變做用域鏈的。以with爲例,在遇到with語句時,會將傳入的對象屬性做爲局部變量來顯示,使其便於訪問,也就是說把一個新的對象添加到了做用域鏈的頂端,這樣必然影響對局部標誌符的解析。with語句執行完畢後,會把做用域鏈恢復到原始狀態

// demo
var name = "global";

// 使用with以前
console.log(name); // 輸出:global

with({name:"jeri"}){
    console.log(name); // 輸出:jeri
}

// 使用with以後,做用域鏈恢復
console.log(name); // 輸出:global

在做用域鏈中有動態做用域時,this引用也會變得更加複雜,再也不指向第一次建立時的上下文,而是由調用者肯定。好比在使用apply或call方法時,傳入它們的第一個參數就是被引用的對象。

function globalThis() {
    console.log(this);
}

globalThis(); // 輸出:Window {document: document,external: Object…}
globalThis.call({name:"clam"}); // 輸出:Object {name: "clam"}
globalThis.apply({name:"clam"},[]); // 輸出:Object {name: "clam"}

由於this引用是動態做用域,因此在編程過程當中必定要注意this引用的變化,及時跟蹤this的變更。

4 .函數做用域

函數做用域,顧名思義就是在定義函數時候產生的做用域,這個做用域也能夠稱爲局部做用域。和全局做用域相反,函數做用域通常只在函數的代碼片斷內可訪問到,外部不能進行變量訪問。在函數內部定義的變量存在於函數做用域中,其生命週期隨着函數的執行結束而結束。

var name = "global";

function fun() {
    var name = "clam";
    console.log(name); // 輸出:jeri

    with ({name:"with"}) {
        console.log(name); // 輸出:with
    }
    console.log(name); // 輸出:clam
}

fun();

// 不能訪問函數做用域
console.log(name); // 輸出:global

5. 沒有塊級做用域

不一樣於其餘編程語言,在JavaScript裏並無塊級做用域,也就是說在for、if、while等語句內部的聲明的變量與在外部聲明是同樣的,在這些語句外部也能夠訪問和修改這些變量的值.

function fun() {
    
    if(0 < 2) {
        var name = "clam";
    }    
    console.log(name); // 輸出:clam
    name = "klay";
    console.log(name); // 輸出:klay
}

fun();

6. 做用域鏈

JavaScript裏一切皆爲對象,包括函數。函數對象和其它對象同樣,擁有能夠經過代碼訪問的屬性和一系列僅供JavaScript引擎訪問的內部屬性。其中一個內部屬性是做用域,包含了函數被建立的做用域中對象的集合,稱爲函數的做用域鏈,它用來保證對執行環境有權訪問的變量和函數的有序訪問

當一個函數建立後,它的做用域鏈會被建立此函數的做用域中可訪問的數據對象填充。在全局做用域中建立的函數,其做用域鏈會自動成爲全局做用域中的一員。而當函數執行時,其活動對象就會成爲做用域鏈中的第一個對象(活動對象:對象包含了函數的全部局部變量、命名參數、參數集合以及this)。在程序執行時,Javascript引擎會經過搜索上下文的做用域鏈來解析諸如變量和函數名這樣的標識符。其會從做用域鏈的最裏面開始檢索,按照由內到外的順序,直到完成查找,一旦完成查找就結束搜索。若是沒有查詢到標識符聲明,則報錯。當函數執行結束,運行期上下文被銷燬,活動對象也隨之銷燬。

var name = 'global';

function fun() {
    console.log(name); // output:global
    name = "change";
    // 函數內部能夠修改全局變量
    console.log(name); // output:change
    // 先查詢活動對象
    var age = "18";
    console.log(age); // output:18
}

fun();

// 函數執行完畢,執行環境銷燬
console.log(age); // output:Uncaught ReferenceError: age is not defined

7. 閉包

閉包是JavaScript的一大謎團,關於這個問題有不少文章進行講述,然而依然有至關數量的程序員對這個概念理解不透徹。閉包的官方定義爲:一個擁有許多變量和綁定了這些變量的環境的表達式(一般是一個函數),於是這些變量也是該表達式的一部分。

一句話歸納就是:閉包就是一個函數,捕獲做用域內的外部綁定。這些綁定是爲以後使用而被綁定,即便做用域已經銷燬。

  • 自由變量

自由變量與閉包的關係是,自由變量閉合於閉包的建立。閉包背後的邏輯是,若是一個函數內部有其餘函數,那麼這些內部函數能夠訪問在這個外部函數中聲明的變量(這些變量就稱之爲自由變量)。然而,這些變量能夠被內部函數捕獲,從高階函數(返回另外一個函數的函數稱爲高階函數)中return語句實現「越獄」,以供之後使用。內部函數在沒有任何局部聲明以前(既不是被傳入,也不是局部聲明)使用的變量就是被捕獲的變量。

function makeAdder(captured) {
    return function(free) {
        var ret = free + captured;
        console.log(ret);
    }
}

var add10 = makeAdder(10);

add10(2); // 輸出:12

從上例可知,外部函數中的變量captured被執行加法的返回函數捕獲,內部函數從未聲明過captured變量,卻能夠引用它。

若是咱們再建立一個加法器將捕獲到同名變量captured,但有不一樣的值,由於這個加法器是在調用makeAdder以後被建立:

var add16 = makeAdder(16);
 
add16(18); // 輸出:34
 
add10(10); // 輸出:20
  • 變量遮蔽

在JavaScript中,當變量在必定做用域內聲明,而後在另外一個同名變量在一個較低的做用域聲明,會發生變量的遮蔽。

var name = "clam";
var name = "klay"

function glbShadow() {
    var name = "fun";

    console.log(name); // 輸出:fun
}

glbShadow();

console.log(name); // 輸出:tom

當在一個變量同一做用域內聲明瞭屢次時,最後一次聲明會生效,會遮蔽之前的聲明。

變量聲明的遮蔽很好理解,然而函數參數的遮蔽就略顯複雜。

var shadowed = 0;

function argShadow(shadowed) {
    var str = ["Value is",shadowed].join(" ");
    console.log(str);
}

argShadow(108); // output:Value is 108

argShadow(); // output:Value is

函數argShadow的參數shadowed覆蓋了全局做用域內的同名變量。即便沒有傳遞任何參數,仍然綁定的是shadowed,並無訪問到全局變量shadowed = 0

任何狀況下,離得最近的變量綁定優先級最高。

var shadowed = 0;

function varShadow(shadowed) {
    var shadowed = 123;
    var str = ["Value is",shadowed].join(" ");
    console.log(str);
}

varShadow(108); // output:Value is 123

varShadow(); // output:Value is 123

varShadow(108)打印出來的並非108而是123,即便沒有參數傳入也是打印的123,先訪問離得最近的變量綁定。

遮蔽變量一樣發生在閉包內部

function captureShadow(shadowed) {

    console.log(shadowed); // output:108
    
    return function(shadowed) {

        console.log(shadowed); // output:2
        var ret = shadowed + 1;
        console.log(ret); // output:3
    }
}

var closureShadow = captureShadow(108);

closureShadow(2);

在編寫JavaScript代碼時,由於變量遮蔽會使不少變量綁定超出咱們的控制,咱們應儘可能避免變量遮蔽,必定要注意變量命名。

  • 典型的誤區

首先看下面的代碼:

var test = function() {
    var ret = [];

    for(var i = 0; i < 5; i++) {
        ret[i] = function() {
            return i;  
        }
    }

    return ret;
};
var test0 = test()[0]();
console.log(test0); // 輸出:5

var test1 = test()[1]();
console.log(test1); //輸出:5

從上面的例子可知,test這個函數執行以後返回一個函數數組,表面上看數組內的每一個函數都應該返回本身的索引值,然而並非如此。當外部函數執行完畢後,外部函數雖然其執行環境已經銷燬,但閉包依然保留着對其中變量綁定的引用,仍然駐留在內存之中。當外部函數執行完畢以後,纔會執行內部函數,而這時內部函數捕獲的變量綁定已是外部函數執行以後的最終變量值了,因此這些函數都引用的是同一個變量i=5

// 更加優雅的描述方式
for(var i = 0; i < 5; i++) {

    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

// 每隔1秒輸出一個5

按照咱們的推斷,上例應該輸出1,2,3,4,5。然而,事實上輸出的是連續5個5。爲何出現這種詭異的情況呢?其本質上仍是由閉包特性形成的,閉包能夠捕獲外部做用域的變量綁定。

上面這個函數片斷在執行時,其內部函數和外部函數並非同步執行的,由於當調用setTimeout時會有一個延時事件排入隊列,等全部同步代碼執行完畢後,再依次執行隊列中的延時事件,而這個時候 i 已經 是5了。

那怎麼解決這個問題呢?咱們是否是能夠在每一個循環執行時,給內部函數傳進一個變量的拷貝,使其在每次建立閉包時,都捕獲一個變量綁定。由於咱們每次傳參不一樣,那麼每次捕獲的變量綁定也是不一樣的,也就避免了最後輸出5個5的情況。實例以下:

for(var i = 0; i < 5; i++) {

    (function(j) {

        setTimeout(function() {
            console.log(j);  
        }, 1000);
    })(i);
}

// 輸出:0,1,2,3,4

閉包具備很是強大的功能,函數內部能夠引用外部的參數和變量,但其參數和變量不會被垃圾回收機制回,常駐內存,會增大內存使用量,使用不當很容易形成內存泄露。但,閉包也是javascript語言的一大特色,主要應用閉包場合爲:設計私有的方法和變量

  • 模擬私有變量

從上文的敘述咱們知道,變量的捕獲發生在建立閉包的時候,那麼咱們能夠把閉包捕獲到的變量做爲私有變量。

var closureDemo = (function() {
    var PRIVATE = 0;

    return {
        inc:function(n) {
            return PRIVATE += n;
        },
        dec:function(n) {
            return PRIVATE -= n;
        }
    };
})();

var testInc = closureDemo.inc(10);
//console.log(testInc);
// 輸出:10

var testDec = closureDemo.dec(7);
//console.log(testDec);
// 輸出:3

closureDemo.div = function(n) {
    return PRIVATE / n;
};

var testDiv = closureDemo.div(3);
console.log(testDiv);
//輸出:Uncaught ReferenceError: PRIVATE is not defined

自執行函數closureDemo執行完畢以後,自執行函數做用域和PRIVATE隨之銷燬,但PRIVATE仍滯留在內存中,也就是加入到closureDemo.incclosureDemo.dec的做用域鏈中,閉包也就完成了變量的捕獲。但以後新加入的closureDemo.div並不能在做用域中繼續尋找到PRIVATE了。由於,函數只有被調用時纔會執行函數裏面的代碼,變量的捕獲也只發生在建立閉包時,因此以後新加入的div方法並不能捕獲PRIVATE

  • 建立特權方法

經過閉包咱們能夠建立私有做用域,那麼也就能夠建立私有變量和私有函數。建立私有函數的方式和聲明私有變量方法一致,只要在函數內部聲明函數就能夠了。固然,既然能夠模擬私有變量和私有函數,咱們也能夠利用閉包這個特性,建立特權方法。

(function() {

    // 私有變量和私有函數
    var privateVar = 10;

    function privateFun() {
        return false;
    };

    // 構造函數
    MyObj = function() {

    };

    // 公有/特權方法
    MyObj.prototype.publicMethod = function() {
        privateVar ++;
        return privateFun();
    }
})();

上面這個實例建立了一個私有做用域,並封裝了一個構造函數和對應的方法。須要注意的是在上面的實例中,在聲明MyObj這個函數時,使用的是不帶var的函數表達式,咱們但願產生的是一個全局函數而不是局部的,否則咱們依然在外部沒法訪問。因此,MyObj就成爲了一個全局變量,可以在外部進行訪問,咱們在原型上定義的方法publicMethod也就可使用,經過這個方法咱們也就能夠訪問私有函數和私有變量了。

總的來講,由於閉包奇特的特性,能夠經過它實現一些強大的功能。但,咱們在平常編程中,也要正確的使用閉包,要時刻注意回收不用的變量,避免內存泄露。

總結

感謝原文做者,而且附上原文連接:JavaScript之做用域與閉包詳解

花費了數小時來研讀這篇分享,對javascript中的做用域和閉包的問題有了比較深入的認識和學習。在從此的編碼過程當中,但願能夠將今天學到的東西及時應用,反覆的鞏固。

相關文章
相關標籤/搜索