深入理解JavaScript系列(3)

2018-06-09 15:54 更新

本文是深入理解JavaScript系列的第篇讀文筆記,博客原文在這里。

內(nèi)容簡要

本文是整個深入理解JavaScript系列中第一篇稍微有點難啃的文章,特別是對新手來說。大叔這篇文章的寫作借鑒了下面兩篇文章,

主要參考的是前者。闡述的內(nèi)容就是模塊化模式。

首先,什么叫模塊化模式?它具有哪些特點?

模塊化模式(或者說模塊化編程)在JavaScript中是一個非常常見的編程技巧,它按照功能(或者業(yè)務(wù))將JavaScript代碼封裝在一起組成一個模塊,對外暴露特定的額方法和屬性。一般來說,模塊化模式具有如下幾個特點,

  • 模塊化,可重用
  • 將一系列的方法及屬性封裝在一起,與其他模塊彼此獨立,不污染全局作用域,松耦合。(大叔這里說的松耦合的意思,我猜測應(yīng)該就是指模塊的獨立性)
  • 暴露特定的接口(方法或者屬性),模塊的其他方法外部不可訪問

BACKBONE

文章的主體將分別介紹模塊化模式的基本用法和高級用法。

基本用法

話不多說,老夫先把大叔的一段代碼拉出來溜溜,

var Calculator = function (eq) {
    /*
    這里可以聲明私有成員
    */
    var eqCtl = document.getElementById(eq);
    // 暴露公開的成員
    return {
        add: function (x, y) {
            var val = x + y;
            eqCtl.innerHTML = val;
        }
    };
};

我們可以通過如下的方式來調(diào)用:

var calculator = new Calculator('eq');
calculator.add(2, 2);

這里使用了new Calculator()把函數(shù)Calculator當(dāng)作一個構(gòu)造函數(shù)調(diào)用。有的同學(xué)可能會問,這里不使用new Calculator(),直接調(diào)用Calculator()函數(shù)好像也是可以的啊,那new Calculator()和不使用new到底啥區(qū)別啊?

首先我在之前的文章中有闡述過new xxx()這個操作的本質(zhì),不太清楚的可以移步看看。

在大叔舉的這個例子中,使用new與不使用new都是沒有錯的,下面的calculator.add方法都是可以調(diào)用的。

不過這兩者有自己適合的場景,

  • 使用new一定會返回一個object,其內(nèi)部的this指向object自身,且object中方法或者屬性可以通過this相互訪問。這種是使用JavaScript構(gòu)建OOP編程的基礎(chǔ)。
  • 不使用new的含義就是簡單的調(diào)用函數(shù),其返回值根據(jù)函數(shù)的return語句來確定,且內(nèi)部的this都指向全局作用域。這種一般用于命名空間的管理,獨立模塊的封裝。

匿名閉包和全局變量

閉包是函數(shù)式編程語言具有的一種語法糖(暫且這么說吧,雖然不太準備)。JavaScript中對閉包的應(yīng)用隨處可見,匿名閉包更是讓一切成為可能的基礎(chǔ),而這也是JavaScript最靈活的特性。

下面我們來創(chuàng)建一個最簡單的閉包函數(shù),函數(shù)內(nèi)部的代碼一直存在于閉包內(nèi),在整個運行周期內(nèi),該閉包都保證了內(nèi)部的代碼處于私有狀態(tài)。

(function () {
    // ...所有的變量和function都在這里聲明,并且作用域也只能在這個匿名閉包里
    // ...但是這里的代碼依然可以訪問外部全局的對象
})();

可以看出匿名閉包其實就是一個自執(zhí)行函數(shù)。關(guān)于自執(zhí)行函數(shù)在后面的文章中還會更加深入的討論。現(xiàn)在只需要知道自執(zhí)行函數(shù)的本質(zhì)是一個函數(shù)表達式,在運行時將會產(chǎn)生一個封閉作用域(或者說私有作用域),這個封閉作用域可以訪問外部的全局變量,但是不能被外部訪問。

這里可能會遇到一個問題,比如下面的代碼,

var name = 'a';
(function() {
    console.log(name); // a
    name = 'b'; // 這里不帶var的賦值,將會將name隱式的提升為全局變量,且覆蓋了閉包外部的name
    console.log(name); // b
})();
console.log(name); // b

可見,由于JavaScript中存在隱式全局變量這樣一種東西,當(dāng)在閉包中需要訪問外部的全局變量時,萬一操作不當(dāng),就會隱式的聲明一個你不知道的全局變量。

現(xiàn)在流行的JavaScript庫,比如JQuery,都采用這樣一種方式來達到從閉包內(nèi)部訪問外部的全局變量的目的,

(function (window, undefined) {
    // JQuery的源碼其實就是一個大閉包?。?})(window);

可見,我們可以將全局變量當(dāng)成一個參數(shù)傳入到匿名函數(shù)然后使用,相比隱式全局變量,它又清晰又快。

有時候可能不僅僅要使用全局變量,而是也想聲明全局變量,如何做呢?我們可以通過將匿名函數(shù)的返回值賦值給這個全局變量,代碼如下,


上面的代碼聲明了一個全局變量blogModule,并且?guī)в?個可訪問的屬性:blogModule.AddTopicblogModule.Name,除此之外,其它代碼都在匿名函數(shù)的閉包里保持著私有狀態(tài)。

高級用法

下面將會闡述幾種稍微高級一點的用法,其實都是對上面所說的基本用法的擴展。

擴展

前面的基本用法可能不太適合大型的項目。因為大型的項目往往會有多人協(xié)作開發(fā),每個人負責(zé)的模塊不盡相同,這時候就需要把一個模塊分割到不同的文件中去。

看下面的代碼,

var blogModule = (function(my) {
    my.AddPhoto = function () {
        //添加內(nèi)部代碼
    };
    return my;
})(blogModule);

發(fā)現(xiàn)了沒有,我們將blogModule自身作為參數(shù)傳遞給自執(zhí)行函數(shù)。這個自執(zhí)行函數(shù)執(zhí)行完畢后,blogModule上就會多處一個方法AddPhoto。試想一下,多人開發(fā)中每個人都采用這種模式,那么完全就可以彼此獨立的給blogModule添加自己所需的方法和屬性。

一般來說,我們還會做一些異常判斷,因為多人協(xié)作的過程中,每個人都不知道自己拿到的blogModule究竟有哪些東西,甚至都不知道這個對象是否為undefined的,改進如下,

var blogModule = (function(my) {
    my.AddPhoto = function () {
        //添加內(nèi)部代碼
    };
    return my;
})(blogModule || {}); // 這里是重點

如上面的代碼,我們在傳遞參數(shù)的時候,其實傳入的是一個判斷表達式,這樣就保證了在閉包內(nèi)部blogModule必定是不為undefined,是不是很巧妙? :)

然而,有的時候我們在擴展的時候需要對某一些方法進行重寫,那么該怎么辦呢?看如下的代碼,

var blogModule = (function (my) {
    var oldAddPhotoMethod = my.AddPhoto;
    my.AddPhoto = function () {
        // 重寫方法,依然可通過oldAddPhotoMethod調(diào)用舊的方法
    };
    return my;
})(blogModule);

代碼中,我們使用私有變量oldAddPhotoMethod緩存了原先的AddPhoto方法,然后重寫了AddPhoto方法。通過這種方式,我們達到了重寫的目的,當(dāng)然如果你想在繼續(xù)在內(nèi)部使用原有的屬性,你可以調(diào)用oldAddPhotoMethod來用。

克隆和繼承

var blogModule = (function (old) {
    var my = {},
        key;
    for (key in old) {
        if (old.hasOwnProperty(key)) {
            my[key] = old[key];
        }
    }
    var oldAddPhotoMethod = old.AddPhoto;
    my.AddPhoto = function () {
        // 克隆以后,進行了重寫,當(dāng)然也可以繼續(xù)調(diào)用oldAddPhotoMethod
    };
    return my;
})(blogModule);

說實話,我有點沒太看懂這部分的內(nèi)容,代碼中的for in循環(huán)其實是將old對象克隆了一份賦值給my對象。但是這個克隆過程中,old對象中的object以及function類型的數(shù)據(jù)其實并沒有發(fā)生克隆,只是多了個my對象的相應(yīng)引用而已。

我不太明白的是,這個跟JavaScript的模塊化有什么關(guān)系呢?望大神解惑。

跨文件共享私有對象

說實話,這部分的我看了很久才弄明白,別看代碼量就10來行,但是真的不是那么好理解的。(也可能是我比較菜。)

這里的需求是這樣的,一個module分割到多個文件中,我們想要各個文件中的私有變量能夠交叉訪問,該怎么做呢?

var blogModule = (function (my) {
    var _private = my._private = my._private || {},
        _seal = my._seal = my._seal || function () {
            delete my._private;
            delete my._seal;
            delete my._unseal;
        },
        _unseal = my._unseal = my._unseal || function () {
            my._private = _private;
            my._seal = _seal;
            my._unseal = _unseal;
        };
    return my;
})(blogModule || {});

任何文件都可以對他們的局部變量_private設(shè)置,并且此設(shè)置對其他的文件也立即生效。一旦這個模塊加載結(jié)束,應(yīng)用會調(diào)用blogModule._seal()進行“上鎖”,這會阻止外部接入內(nèi)部的_private。如果這個模塊需要再次增生,應(yīng)用的生命周期內(nèi),任何文件都可以調(diào)用_unseal()進行“開鎖”,然后再加載新文件,加載新文件后又會初始化_pravite_seal_unseal,然后再次調(diào)用_seal()“上鎖”。

這個過程很巧妙。其實我覺得,各個文件中的私有變量能夠交叉訪問這個需求簡直就是個奇葩,為什么各個文件中的私有變量要交叉訪問呢?這不是破壞了模塊的獨立性么?有人說其實這里的多文件其實仍然描述的是同一個模塊,那我想說,這種情況下使用子模塊應(yīng)該是一種更優(yōu)的選擇。

子模塊

子模塊是最簡單也是最經(jīng)常使用的設(shè)計思路,

blogModule.subModule = (function () {
    var my = {};
    // ...
    return my;
})();

其實我個人覺得,子模塊+模塊擴展就足夠應(yīng)付一般的模塊化模式需求了。使用過多的高級用法,必定給代碼增加復(fù)雜度,給日后的維護和升級肯定為增加難度。

總結(jié)

就我目前的經(jīng)驗來說,模塊化模式經(jīng)常用到的幾種方法包括,松耦合擴展私有作用域以及子模塊。這幾種就是最常用的方式。

不過現(xiàn)在業(yè)界出現(xiàn)了CMD、AMD等規(guī)范后,JavaScript代碼的模塊化管理更趨向于代碼級別而不再是設(shè)計級別。所以本文中所描述的模塊化模式,我猜測日后將會越來越淡化,越來越輕量化,比如用在配置中,工具方法的管理上等等。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號