JS模塊化和閉包


JS模塊化和閉包

js最初作為一個在瀏覽器中運行的腳本語言,設計的目標是用來給html增加交互行為,早期的網站都是在服務器端生成並返回給瀏覽器,js也只對單獨的一個html進行操作,所以模塊化並沒有在早期的JS中得到很好的考慮,隨着瀏覽器js引擎越發的快速,現在已經有很多前端框架,並不依賴與服務器生成html,而是自己直接通過js來生成,最典型的例子就是我們常聽到的webapp。現在所有的js庫都包裝的非常好了,我們今天看看一些js模塊化的基礎知識吧。

名稱空間namespace

在js中如何實現命名空間,我們來看一個例子。

<!DOCTYPE html>  
<html>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
    <body>

    <button id="mybtn" onclick="display();">點我加載列表</button>
    <h1>我想要不死族的英雄</h1>
    <ul id="mylist"></ul>   
    
    <script type="text/javascript" src="./namespacejs1.js"></script>    
    <script type="text/javascript" src="./namespacejs2.js"></script>    
    </body>  
</html>

我們先定義一個html文件。點擊里面的按鈕,顯示魔獸爭霸的軍隊。響應按鈕的代碼分別定義在namespacejs1.js和namespacejs2.js兩個文件中。
這是namespacejs1.js

var list = ['死亡騎士','巫妖','恐懼魔王'];
function display(){
    var mylist = document.getElementById("mylist"), 
    fragment = document.createDocumentFragment(), 
    element; 
    for(var i = 0, x = list.length; i<x; i++){ 
        element = document.createElement("li"); 
        element.appendChild( document.createTextNode( list[i]) ); 
        fragment.appendChild(element); 
    }
    console.log(importGlobalVariable);
    mylist.appendChild(fragment);
}

這是namespacejs2.js

var list = ['聖騎士','大法師','山丘之王'];

當我們點擊按鈕的時候,會顯示什么東東呢。我們期望顯示不死族的英雄(namespacejs1.js中的list),但是這里會顯示人類的英雄(namespacejs2.js中的list)。顯然,我們想要的數據被覆蓋了。怎么樣才能解決這個問題呢?我們來加上名稱空間吧。看看改進后的namespacejs1.js。

var namespace1 = {
    list : ['死亡騎士','巫妖','恐懼魔王'],
    display : function display(){
        var mylist = document.getElementById("mylist"), 
        fragment = document.createDocumentFragment(), 
        element; 

        for(var i = 0, x = list.length; i<x; i++){ 
            element = document.createElement("li"); 
            element.appendChild( document.createTextNode( this.list[i]) ); 
            fragment.appendChild(element); 
        }

        mylist.appendChild(fragment);
    }
};

這時候點擊按鈕會沒有用,需要稍微改動一下

<button id="mybtn" onclick="namespace1.display();">點我加載列表</button>

可以看到display在namespace1的名稱空間下面了。問題已經圓滿解決。但是如果我們想擴展namespace1的功能怎么辦呢,難道只能修改namespace1.js的源文件嗎,還有list很不安全啊,誰都可以改動它,如何把它變成私有變量呢?

js的封裝

在js中並沒有私有公有的直接支持,但是不代表js語言不能完成這個。我們看一下如何隱藏list。

var namespace1 = (function(){
    
    var importGlobalVariable = gv;
    var list = ['死亡騎士','巫妖','恐懼魔王'];

    function display(){
        var mylist = document.getElementById("mylist"), 
        fragment = document.createDocumentFragment(), 
        element; 
        for(var i = 0, x = list.length; i<x; i++){ 
            element = document.createElement("li"); 
            element.appendChild( document.createTextNode( list[i]) ); 
            fragment.appendChild(element); 
        }
        mylist.appendChild(fragment);
    }
    return {
        display:display
    };
})();

這段代碼返回了一個對象,在這個對象中我們只能訪問display方法,是不是很牛逼呢,這樣就解決隱藏問題。

js模塊的擴展

我們知道js的繼承是用原型鏈來實現的,但是這里要討論的是模塊的擴展,所以這邊不會說道繼承的問題。如何擴展namespace1的功能呢。我么看一下下面的代碼。

var namespace1 = (function(n1){
    
    var listArmy = ['4個蜘蛛','2個食屍鬼','2個冰龍']

    n1.displayArmy = function(){
        n1.display();
        var mylist = document.getElementById("mylist"), 
            fragment = document.createDocumentFragment(), 
            element; 
        for(var i = 0, x = listArmy.length; i<x; i++){ 
            element = document.createElement("li"); 
            element.appendChild( document.createTextNode( listArmy[i]) ); 
            fragment.appendChild(element); 
        }
        mylist.appendChild(fragment);
    }

    return n1;
})(namespace1);

這段代碼我們用來增加一個displayArmy的方法,用來顯示不死族軍隊,也就是listArmy的數據吧。
我們看到上面的代碼把namespace1作為參數傳入到一個立刻調用函數中,這樣在里面給它增加一個函數。有沒有感覺js很強大啊。

閉包(Closures)

如果已經習慣了C,C++,C#或者Java,那么上面的實現簡直匪夷所思,感覺變量的作用域和生命周期都很奇怪。那么我們說說js中的一個重要概念閉包吧。

定義

Closures are functions that refer to independent (free) variables.
這里就不翻譯了,因為翻譯過來實在是很奇怪,比如,閉包是那些引用了獨立變量的函數。那么按照定義是否可以認為函數就是閉包呢,為了搞清楚閉包的概念,我們需要了解函數對象,變量生命周期,和嵌套的作用域三個概念。

函數對象

什么是函數對象呢,在C語言中和C++語言中,可以想函數那樣用()去調用,看起來和函數一樣的對象就叫函數對象。比如在C語言中:

typedef void (*func_t)(int); //定義函數指針
//定義一個函數
void f(int n){
    printf("node(?)=%d\n",n);
}
int main(){
    func_t pf = f;
    f(1);
}

可以看到f很像一個函數吧,但是呢,其實它只是一個函數指針而已。在C++語言中,我們也看一個例子。

class Foreach
{
private:
    struct node * myList;

public:
    Foreach(){
    }
    ~Foreach(){}
    void operator()(){
        //做點有意義的事情吧
    }
};
int main(){
    Foreach *foreach = new Foreach();
    (*foreach)();
}

可以看到foreach對象也很像函數。
我們在看一下js中的函數對象吧。

var f = function(){};
f();

看看,是不是很簡單。

嵌套的作用域

我們已經看到js的函數對象的定義已經很方便了,那么還有什么和傳統語言不一樣的地方呢?
我們看一下這個代碼。

var x = 10;
function f(){
    y=15;
    function g(){
        var z=25;
        alert(x+y+z);
    }
    g();
}
f();//顯示50

這段代碼在C語言中沒法實現,原因是C語言的變量無法訪問當前作用域外的變量。也就是說函數g里面訪問不了y,函數f也訪問不了x。但是js卻能做到。我們看一下這樣有什么強大的地方。

#include <stdio.h>
#include <stdlib.h>

struct node{
    struct node *next;
    int val;
};

// 函數指針,JS中的函數對象
typedef void (*func_t)(int); 

void foreach(struct node *list, func_t func){
    while(list){
        func(list->val);
        list = list->next;
    }
}
void f(int n){
    printf("node(?)=%d\n",n);
}
int main(){
    func_t pf = f;
    f(1);
    
    // bool b = false;
    // b = true;
    // b = "zifuchuan";

    struct node * list = 0, *l;
    int i;

    for(i=0; i<4; i++){
        l = malloc(sizeof(struct node));
        l->val = i;
        l->next = list;
        list = l;
    }

    i=0;l=list;
    //這個循環可以打印出index,也就是i,如下是打印結果
    //node(0) = 3
    //node(1) = 2
    //node(2) = 1
    //node(3) = 0
    while(l){
        printf("node(%d) = %d\n", i++, l->val);
        l = l->next;
    }

    //foreach里面再調用函數f,就不能訪問i了,如下是打印結果
    //node(?)=3
    //node(?)=2
    //node(?)=1
    //node(?)=0
    foreach(list,f);
}

我們看到C語言的高階函數(函數調用函數)是沒法訪問外部變量的。那么js寫這段代碼怎么弄呢?

function foreach(list, func){
    while(list){
        func(list.val);
        list=list.next;
    }
}

var i = 0;
//這里可以使用變量i
foreach(list,function(n){
    console.log("node("+ i +") = " +n);
    i++;
});

我們發現js的變量作用域比C語言要牛逼一點吧。

生命周期

下面再看看js閉包的更牛逼的地方吧。

function extent(){
    var n=0;
    return function (){
        n++;
        console.log("n="+n);
    }
}
f = extent();
f();//=>n=1
f();//=>n=2

這里,當extent函數執行完畢后,n變量應該掛了才對,但是,我們通過結果看到,n變量還活的好好的呢。

總結

屬於外部作用域的局部變量,被函數對象給“封閉”在里面了。閉包(“closure”)這個詞原本就是封閉的意思。被封閉起來的變量的壽命,與封閉它的函數對象的壽命相等。也就是說,當封閉這個變量的函數對象不再被訪問,被垃圾回收器回收掉時,這個變量才掛。
現在大家明白閉包的定義了吧。在函數對象中,將局部變量封閉起來的結構被叫做閉包。因此,C語言的函數指針並不是閉包,JavaScript中的函數對象才是閉包。另外C++等傳統面向對象語言,也加入了對函數式編程的支持,其中一方面就是Lambda表達式,也有閉包的概念。

全局變量的導入

現在大家理解閉包的概念了,我們看看全局變量導入的問題。因為閉包中內部變量會一直持有外部變量,所以我們最好把外部變量當做參數傳遞給我們要使用的內部函數,這樣會節省內存和查找變量的時間。因為找到一個外部變量,需要從內部往外部一層層的查找,很費時間(對解析器來說)。另外,一起開發代碼的人也會很迷惑這個變量到底在那里定義的,容易出錯,我們來看一個例子。

var globalVariable = "我是外層變量,從disply函數找到我需要很久很久";

var namespace1 = (function(gv){
    
    var importGlobalVariable = gv;
    var list = ['死亡騎士','巫妖','恐懼魔王'];

    function display(){
        var mylist = document.getElementById("mylist"), 
        fragment = document.createDocumentFragment(), 
        element; 
        for(var i = 0, x = list.length; i<x; i++){ 
            element = document.createElement("li"); 
            element.appendChild( document.createTextNode( list[i]) ); 
            fragment.appendChild(element); 
        }
        console.log(importGlobalVariable);
        mylist.appendChild(fragment);
    }
    return {
        display:display
    };
})(globalVariable);

這個例子把globalVariable當成參數傳入,這樣他就在匿名function函數的作用域里面了。
在看一個圖,會更直觀
嵌套作用域的查找
我們不希望解析器去一直往上查找,所以幫幫它嘍。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM