名稱空間管理


ECMAScript 6 才有規範模組語法,在這之前,JavaScript 本身沒有名稱空間管理的機制,名稱都是物件上的特性,要不就是全域物件上的特性(全域變數),要不就是 context 物件上的變數(區域變數)。

名稱衝突的問題極容易在 JavaScript 中發生,就算是在同一個 .js 檔案中也有可能發生。例如你也許寫了個 validate 函式,假以時日別人接手你的程式,然後在檔案中某處又定義了另一個 validate 函式:

function validate() {
    //.. 作些驗證
}
// 很長很長的程式。。
// 某年某月的某一天。。
function validate() {
    //.. 作些別的驗證
}

很不幸地,之後的 validate 函式定義會覆蓋前一個函式,也許會讓之前可以動作的功能失效。

最基本的名稱空間管理,就是將這個函式作為某物件的特性,該物件是對組織或單位有意義的名稱所參考。例如:

var openhome = {};     // 作為名稱空間
function validate() {
    //.. 作些驗證
}
openhome.validate = validate;

想要取用你定義的 validate 函式,則可以如下:

openhome.validate();

其他人在定義函式時,也可以作類似考量。例如他也許在同一個 .js 中如下定義:

var caterpillar = {};     // 作為名稱空間
function validate() {
    //.. 作些驗證
}
caterpillar.validate = validate;

呼叫時就使用:

caterpillar.validate();

如此就不會發生名稱覆蓋的問題。或許在同一個 .js 中,以上名稱衝突的情況比較算少數,通常這會發生在兩個 .js 分別由兩個不同組識或作者撰寫時。

以上是在 ES6 之前,進行名稱空間處理的出發點,也就是透過特定物件來收納相關 API,然後該物件被指定給一個名稱。

在上面的範例中,validate 的函式宣告,實際上還是在全域佔用了一個 validate 名稱,你可以這麼解決:

var openhome = {};
openhome.validate = function() {
    // 作些驗證
};

但如果這個函式也想作為其他物件上的特性,函式實字的作法會比較不方便。有個方式可以比較漂亮地解決。例如:

var openhome = (function() {
    function validate() {
        // 作些驗證
    }

    function fomrat() {
        // 作些格式化
    }

    // ... 其他...

    return {
        validate : validate,
        format   : format
    };
})();

乍看有些複雜,事實上,首先是撰寫..

function() {
}

這寫下了一個函式實字,沒有名稱參考至它,所以不會污染全域名稱空間…接著…

(function() {
})

加上括號是語法需求,這樣 JavaScript 引擎才知道這邊是個函式實字,接著…

(function() {
})();

最後的括號表示呼叫傳回的函式物件,有人稱這樣的寫法為 IIFE(Immediately Invoked Function Expression),也就是所謂的立即呼叫函式,在這個函式中建立的名稱,範圍都是在函式中,不會污染全域名稱空間,到最後傳回的物件被指定給 openhome 變數。

通常你會在以上的模式中,撰寫一個模組,也就是將功能相關的程式碼寫在 IIFE 中,如果想要擴充這個模組,同樣也可以在 IIFE 中進行。例如:

var openhome = (function() {
    function validate() {
        // 作些驗證
    }

    function fomrat() {
        // 作些格式化
    }

    // ... 其他...

    return {
        validate : validate,
        format   : format
    };
})();

// 擴充模組
(function(module) {
    function find() {
        // ...找些東西
    }

    function map() {
        // ... 作些對應
    }

    // .... 其他

    module.find = find;
    module.map = map;

})(openhome);

這麼一來,就可以基於原本的模組來擴充功能,最後,可以來做些正事了:

(function(module) {

    // 應用程式流程

})(openhome);

現在的問題在於,如果連 openhome 這樣的名稱都不想佔用呢?那麼需要有個模組管理程式,來負責管理這些名稱,例如,一個最簡單的實現是:

var define, require;

(function() {
    var modules = {};

    define = function(name, callback) {
        modules[name] = callback();
    };

    require = function(name, callback) {
        callback(modules[name]);
    };
})();

接下來,如果要定義一個模組,例如,仍然是定義 openhome 模組,就可以使用 define

define('openhome', function() {
    function validate() {
        console.log('validate');
    }

    function format() {
        console.log('format');
    }

    // ... 其他...

    return {
        validate : validate,
        format   : format
    };
});

而另一個模組,想要擴充 openhome 模組,或者相依於 openhome 模組來做些事情,可以使用 require

require('openhome', function(openhome) {
    openhome.validate();
    openhome.format();
    // ... 其他
});

當然,相依的模組也許不只一個,這就需要修改一下 require

var define, require;

(function() {
    var modules = {};

    define = function(name, callback) {
        modules[name] = callback();
    };

    require = function(names, callback) {
        var dependencies = names.map(function(name) {
            return modules[name];
        });

        callback.apply(undefined, dependencies);
    };
})();

define('openhome', function() {
    function validate() {
        console.log('validate');
    }

    function format() {
        console.log('format');
    }

    // ... 其他...

    return {
        validate : validate,
        format   : format
    };
});

define('caterpillar', function() {
    function foo() {
        console.log('foo');
    }

    // ... 其他...

    return {
        foo : foo
    };
});

require(['openhome', 'caterpillar'], function(openhome, caterpillar) {
    openhome.validate();
    openhome.format();
    caterpillar.foo();
});

如果在瀏覽器的環境中,模組管理程式可能是放在一個 require.js 裏:

var define, require;

(function() {
    var modules = {};

    define = function(name, callback) {
        modules[name] = callback();
    };

    require = function(names, callback) {
        var dependencies = names.map(function(name) {
            return modules[name];
        });

        callback.apply(undefined, dependencies);
    };
})();

而你會在 openhome.js 放入:

define('openhome', function() {
    function validate() {
        console.log('validate');
    }

    function format() {
        console.log('format');
    }

    // ... 其他...

    return {
        validate : validate,
        format   : format
    };
});

在 caterpillar.js 中放入:

define('caterpillar', function() {
    function foo() {
        console.log('foo');
    }

    // ... 其他...

    return {
        foo : foo
    };
});

接著寫一個 main.js:

require(['openhome', 'caterpillar'], function(openhome, caterpillar) {
    openhome.validate();
    openhome.format();
    caterpillar.foo();
});

最後,在網頁中,你會這麼撰寫:

<script type="text/javascript" src="require.js"></script>
<script type="text/javascript" src="openhome.js"></script>
<script type="text/javascript" src="caterpillar.js"></script>
<script type="text/javascript" src="main.js"></script>

當然,安排模組 .js 檔案的順序也會是個問題,你可以繼續依需求來重構下去,直到滿足需求為止,實際上,已經有許多成熟的模組實現,可以用來管理名稱空間,而且進一步地處理了模組依賴、載入等需求,例如,在瀏覽器的環境中,許多開發者熟悉的是 RequireJS,它實現了 AMD(Asynchronous Module Definition)。

而在 Node.js 中,實現的是 CommonJS 的模組規範,另外,也存在著 UMD(Universal Module Definition)的形式,可以在 AMD 和 CommonJS 之間溝通。

ES6 本身也提供了模組方面的規範,然而在後端,與 Node.js 现存的 CommonJS 有許多不同,而在前端,一些長青(Even-green)瀏覽器,實現了 ES6 模組的規範,不過考慮到瀏覽器之間的相容性,使用 RequireJS 會是比較保險的做法。

或者,你可以固定採用某個規範,然而在必要時,借助轉譯或模組封裝工具(像是 Babel 或 Webpack),將模組轉換為另一個環境或規範。