JavaScript 做用域和閉包理解

做用域:

分爲函數做用域,和塊級做用域;緩存

函數做用域

函數做用域外面的沒法訪問函數做用域內部的變量和函數,這樣就能夠將一些變量和函數隱藏起來;bash

隱藏起來的好處是閉包

  1. 造成命名空間,避免各個函數裏面的變量衝突
  2. 實現模塊管理

內部能夠訪問外部的;app

function foo(a) { var b = 2;
    // 一些代碼
    function bar() {
    // ...
    }
    // 更多的代碼 var c = 3;
}

bar(); // 失敗
console.log( a, b, c ); // 三個全都失敗

複製代碼

此時,foo裏面就是一個函數做用域,能夠bar裏面又是一個做用域;最外面固然就是全局做用域;函數

能夠把函數看着一個能夠單向向外訪問的圈子;ui

函數表達式 vs 函數聲明

這裏須要重點區分一下:spa

  1. 函數聲明: function 是聲明中 的第一個詞,那麼就是一個函數聲明;
  2. 函數表達式:除此以外就是函數表達式;
  3. 函數表達式能夠是匿名的,函數聲明不能夠;

函數聲明和函數表達式之間最重要的區別是它們的名稱標識符將會綁定在何處;code

例子對象

var a = 2;
(function foo(){ // <-- 添加這一行 
    var a = 3;
    console.log( a ); // 3
})(); // <-- 以及這一行 
console.log( a ); // 2
複製代碼
  1. 這裏的(function foo(){ .. })是一個函數表達式;而不是一 個標準的函數聲明; 因此,foo 被綁定在函數表達式自身的函數中而不是所在做用域中。ip

  2. 換句話說,(function foo(){ .. })做爲函數表達式意味着foo只能在..所表明的位置中被訪問,外部做用域則不行。foo 變量名被隱藏在自身中意味着不會非必要地污染外部做 用域。

  3. 當即執行函數表達式:因爲foo被包含在一對( )括號內部,所以成爲了一個表達式;經過在末尾加上另一個 ( ) 能夠當即執行這個函數;即, (function foo(){ .. })()。第一個 ( ) 將函數變成表 達式,第二個 ( ) 執行了這個函數。

  4. 還能夠穿參數

var a = 2;
(function IIFE( global ) {
    var a = 3;
    console.log( a ); // 3 
    console.log( global.a ); // 2
})( window );
console.log( a ); // 2
複製代碼

這樣就能夠訪問外面的a了,由於訪問變量a的時候就近原則,就獲得了3;

塊級做用域

1. { }

var 定義的變量和函數,和在當前塊級所處做用域定義沒有什麼區別; let,const 在塊級做用域外面就訪問不到;

簡單的說{ }這個就造成了一個塊級做用域;

例子

{
    var a = 44;
    let b = 22;
    const c = 33
}
a // 44;
b // Uncaught ReferenceError: b is not defined;找不到引用,報錯;
c // Uncaught ReferenceError: c is not defined;找不到引用,報錯;
複製代碼

2. with

用 with 從對象中建立出的做用域僅在 with 聲明中而非外 部做用域中有效。

3. try/catch

JavaScript 的 ES3 規範中規定 try/catch 的 catch 分句會建立一個塊做 用域,其中聲明的變量僅在 catch 內部有效。

閉包

不管論經過何種手段將內部函數傳遞到所在的做用域之外,它都會持有對原始定義做用域的引用,不管在何處執行這個函數都會使用閉包。

我的所理解的閉包就是一個做用域,及做用域內的變量和函數的緩存;不會被釋放了,以供在從此訪問;不得被垃圾回收掉

不知道你們是否還記得JavaScript的垃圾回收機制;

垃圾收集:就是執行完後,對沒有引用的變量進行釋放;常見手動釋放就是設置成null;

例子1

function foo() { 
    var a = 2;
    function bar() { 
        console.log( a );
    }
    return bar; 
}
var baz = foo();
baz(); // 2 —— 朋友,這就是閉包的效果。
複製代碼

分析:

  1. 函數 bar() 的做用域可以訪問 foo() 的內部做用域。 bar() 顯然能夠被正常執行。而且,它能在本身定義的做用域之外的地方 執行。
  2. 在 foo() 執行後,一般會期待foo()的整個內部做用域都被銷燬,由於咱們知道引擎有垃圾回收器用來釋放再也不使用的內存空間。因爲看上去 foo() 的內容不會再被使用,因此很天然地會考慮對其進行回收。
  3. 而閉包的「神奇」之處正是能夠阻止這件事情的發生。事實上內部做用域依然存在,所以沒有被回收。誰在使用這個內部做用域?原來是 bar() 自己在使用。
  4. 拜 bar() 所聲明的位置所賜,它擁有涵蓋foo()內部做用域的閉包,使得該做用域可以一 直存活,以供 bar() 在以後任什麼時候間進行引用。

bar() 依然持有對該做用域的引用,而這個引用就叫做閉包。

例子2

function foo() { 
    var a = 2;
    function baz() {
        console.log( a ); // 2
    }
    bar( baz ); 
}
function bar(fn) {
    fn(); // 媽媽快看呀,這就是閉包!
}
foo(); // 2
複製代碼

等效以下:

var fn;
function foo() {
    var a = 2;
    function baz() { 
        console.log( a );
    }
    fn = baz; //將baz分配給全局變量 
}
function bar() {
    fn(); // 媽媽快看呀,這就是閉包!
}
foo();
bar(); // 2
複製代碼

按正常的做用域思考方式,bar是沒有辦法訪問foo的內部的變量的;

  1. 可是foo能夠訪問外部做用域下的bar;
  2. bar在foo內部;
  3. 將baz傳遞給bar的內部,baz不管在哪裏都依然持有對foo內部變量的引用

baz 和 變量a,還有foo造成了一個閉包,這個做用域將被引擎緩存起來;baz隨時均可以訪問;

function foo(a) { 
    var b = 2;
    // 一些代碼
    function bar() {
        // ...
    }
    // 更多的代碼 
    var c = 3;
}
foo();
bar(); // 失敗
console.log( a, b, c ); // 三個全都失敗
複製代碼

這種就沒法訪問foo裏面的變量和函數了,由於foo裏面都是局部變量,外部沒法直接訪問,這種裏面變量再會被其餘地方引用,將會被引擎垃圾回收釋放掉。

造成閉包的條件

  1. 一個函數foo包含一個函數baz和一個變量a;(名字隨意)
  2. baz內部存在對a的引用;
  3. foo須要被執行;

正確例子示範

function wait(message) {
    setTimeout( function timer() {
        console.log( message );
    }, 1000 ); 
}

wait( "Hello, closure!" );
複製代碼

這就是個閉包;

  1. timer和message都在wait內部
  2. timer對wait的message有引用;
  3. wait被執行了

通常來講,只要使 用了回調函數,實際上就是在使用閉包!

錯誤例子示範

for (var i=1; i<=5; i++) { 
    (function() {
        setTimeout( function timer() { 
            console.log( i );
        }, i*1000 );
    })();
}
// 打印5次6;
複製代碼

這樣不行!這只是一個都沒有的空做用域。不能造成閉包

修改1:

for (var i=1; i<=5; i++) { 
    (function(j) {
        setTimeout( function timer() { 
            console.log( j );
        }, j*1000 );
    })(i);//從外部傳進來
}
複製代碼

修改2:

for (let i=1; i<=5; i++) { 
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}
//塊做用域和閉包聯手即可天下無敵
複製代碼

應用——模塊

模塊有兩個主要特徵:

  1. 爲建立內部做用域而調用了一個包裝函數;
  2. 包裝函數的返回 值必須至少包括一個對內部函數的引用,這樣就會建立涵蓋整個包裝函數內部做用域的閉包。
var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i=0; i<deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply( impl, deps );
    }
    function get(name) { 
        return modules[name];
    }
    return {
        define: define,
        get: get
    };
})();

MyModules.define( "bar", [], function() { 
    function hello(who) {
        return "Let me introduce: " + who;
    }
    return {
     hello: hello
    }; 
} );

MyModules.define( "foo", ["bar"], function(bar) {
    var hungry = "hippo";
    function awesome(){
        console.log( bar.hello( hungry ).toUpperCase() )
    }
    return {
        awesome: awesome
    }; 
} );

var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(
    bar.hello( "hippo" )
); // Let me introduce: hippo 
foo.awesome(); // LET ME INTRODUCE: HIPPO
複製代碼

"foo" 和 "bar" 模塊經過一個返回公共 API 的函數來定義的。"foo" 甚至接受 "bar" 的 示例做爲依賴參數,並能相應地使用它。

總結

最後記住:當函數能夠記住並訪問所在的做用域,即便函數是在當前做用域以外執行,這時 就產生了閉包。

相關文章
相關標籤/搜索