JavaScript設(shè)計(jì)模式之職責(zé)鏈模式詳解
職責(zé)鏈模式
職責(zé)鏈模式的定義是:使多個(gè)對(duì)象都有機(jī)會(huì)處理請(qǐng)求,從而避免請(qǐng)求的發(fā)送者和接收者之間的耦合關(guān)系,將這些對(duì)象連成一條鏈,并沿著這條鏈傳遞該請(qǐng)求,直到有一個(gè)對(duì)象處理它為止。
職責(zé)鏈模式的名字非常形象,一系列可能會(huì)處理請(qǐng)求的對(duì)象被連接成一條鏈,請(qǐng)求在這些對(duì)象之間依次傳遞,直到遇到一個(gè)可以處理它的對(duì)象,我們把這些對(duì)象稱為鏈中的節(jié)點(diǎn),如下圖所示。
1. 現(xiàn)實(shí)中的職責(zé)鏈模式
職責(zé)鏈模式的例子在現(xiàn)實(shí)中并不難找到,以下就是常見(jiàn)的跟職責(zé)鏈模式有關(guān)的場(chǎng)景。
如果早高峰能順利擠上公交車的話,那么估計(jì)這一天都會(huì)過(guò)得很開(kāi)心。因?yàn)楣卉嚿先藢?shí)在太多了,經(jīng)常上車后卻找不到售票員在哪,所以只好把兩塊錢硬幣往前面遞。除非你運(yùn)氣夠好,站在你前面的第一個(gè)人就是售票員,否則,你的硬幣通常要在 N 個(gè)人手上傳遞,才能最終到達(dá)售票員的手里。
從這個(gè)例子中,我們很容易找到職責(zé)鏈模式的最大優(yōu)點(diǎn):請(qǐng)求發(fā)送者只需要知道鏈中的第一個(gè)節(jié)點(diǎn),從而弱化了發(fā)送者和一組接收者之間的強(qiáng)聯(lián)系。如果不使用職責(zé)鏈模式,那么在公交車上,我就得先搞清楚誰(shuí)是售票員,才能把硬幣遞給他。
2. 實(shí)際開(kāi)發(fā)中的職責(zé)鏈模式
假設(shè)我們負(fù)責(zé)一個(gè)售賣手機(jī)的電商網(wǎng)站,經(jīng)過(guò)分別交納 500 元定金和 200 元定金的兩輪預(yù)定后(訂單已在此時(shí)生成),現(xiàn)在已經(jīng)到了正式購(gòu)買的階段。
公司針對(duì)支付過(guò)定金的用戶有一定的優(yōu)惠政策。在正式購(gòu)買后,已經(jīng)支付過(guò) 500 元定金的用戶會(huì)收到 100 元的商城優(yōu)惠券,200 元定金的用戶可以收到 50 元的優(yōu)惠券,而之前沒(méi)有支付定金的用戶只能進(jìn)入普通購(gòu)買模式,也就是沒(méi)有優(yōu)惠券,且在庫(kù)存有限的情況下不一定保證能買到。
我們的訂單頁(yè)面是 Node 吐出的模板,在頁(yè)面加載之初,Node 會(huì)傳遞給頁(yè)面幾個(gè)字段。
orderType
:表示訂單類型(定金用戶或者普通購(gòu)買用戶),code 的值為 1 的時(shí)候是 500 元定金用戶,為 2 的時(shí)候是 200 元定金用戶,為 3 的時(shí)候是普通購(gòu)買用戶。pay
:表示用戶是否已經(jīng)支付定金,值為true
或者false
, 雖然用戶已經(jīng)下過(guò) 500 元定金的訂單,但如果他一直沒(méi)有支付定金,現(xiàn)在只能降級(jí)進(jìn)入普通購(gòu)買模式。stock
:表示當(dāng)前用于普通購(gòu)買的手機(jī)庫(kù)存數(shù)量,已經(jīng)支付過(guò) 500 元或者 200 元定金的用戶不受此限制。
下面我們把這個(gè)流程寫成代碼:
const order = function (orderType, pay, stock) { if (orderType === 1) { // 500 元定金購(gòu)買模式 if (pay === true) { // 已支付定金 console.log('500 元定金預(yù)購(gòu), 得到 100 優(yōu)惠券'); } else { // 未支付定金,降級(jí)到普通購(gòu)買模式 if (stock > 0) { // 用于普通購(gòu)買的手機(jī)還有庫(kù)存 console.log('普通購(gòu)買, 無(wú)優(yōu)惠券'); } else { console.log('手機(jī)庫(kù)存不足'); } } } else if (orderType === 2) { // 200 元定金購(gòu)買模式 if (pay === true) { console.log('200 元定金預(yù)購(gòu), 得到 50 優(yōu)惠券'); } else { if (stock > 0) { console.log('普通購(gòu)買, 無(wú)優(yōu)惠券'); } else { console.log('手機(jī)庫(kù)存不足'); } } } else if (orderType === 3) { if (stock > 0) { console.log('普通購(gòu)買, 無(wú)優(yōu)惠券'); } else { console.log('手機(jī)庫(kù)存不足'); } } }; order(1, true, 500); // 輸出: 500 元定金預(yù)購(gòu), 得到 100 優(yōu)惠券
雖然我們得到了意料中的運(yùn)行結(jié)果,但這遠(yuǎn)遠(yuǎn)算不上一段值得夸獎(jiǎng)的代碼。order
函數(shù)不僅巨大到難以閱讀,而且需要經(jīng)常進(jìn)行修改。雖然目前項(xiàng)目能正常運(yùn)行,但接下來(lái)的維護(hù)工作無(wú)疑是個(gè)夢(mèng)魘??峙轮挥凶?ldquo;新手”的程序員才會(huì)寫出這樣的代碼。
3. 用職責(zé)鏈模式重構(gòu)代碼
現(xiàn)在我們采用職責(zé)鏈模式重構(gòu)這段代碼,先把 500 元訂單、200 元訂單以及普通購(gòu)買分成 3 個(gè)函數(shù)。
接下來(lái)把 orderType
、pay
、stock
這 3 個(gè)字段當(dāng)作參數(shù)傳遞給 500 元訂單函數(shù),如果該函數(shù)不符合處理?xiàng)l件,則把這個(gè)請(qǐng)求傳遞給后面的 200 元訂單函數(shù),如果 200 元訂單函數(shù)依然不能處理該請(qǐng)求,則繼續(xù)傳遞請(qǐng)求給普通購(gòu)買函數(shù),代碼如下:
// 500 元訂單 const order500 = function (orderType, pay, stock) { if (orderType === 1 && pay === true) { console.log('500 元定金預(yù)購(gòu), 得到 100 優(yōu)惠券'); } else { order200(orderType, pay, stock); // 將請(qǐng)求傳遞給 200 元訂單 } }; // 200 元訂單 const order200 = function (orderType, pay, stock) { if (orderType === 2 && pay === true) { console.log('200 元定金預(yù)購(gòu), 得到 50 優(yōu)惠券'); } else { orderNormal(orderType, pay, stock); // 將請(qǐng)求傳遞給普通訂單 } }; // 普通購(gòu)買訂單 const orderNormal = function (orderType, pay, stock) { if (stock > 0) { console.log('普通購(gòu)買, 無(wú)優(yōu)惠券'); } else { console.log('手機(jī)庫(kù)存不足'); } }; // 測(cè)試結(jié)果: order500(1, true, 500); // 輸出:500 元定金預(yù)購(gòu), 得到 100 優(yōu)惠券 order500(1, false, 500); // 輸出:普通購(gòu)買, 無(wú)優(yōu)惠券 order500(2, true, 500); // 輸出:200 元定金預(yù)購(gòu), 得到 500 優(yōu)惠券 order500(3, false, 500); // 輸出:普通購(gòu)買, 無(wú)優(yōu)惠券 order500(3, false, 0); // 輸出:手機(jī)庫(kù)存不足
可以看到,執(zhí)行結(jié)果和前面那個(gè)巨大的 order
函數(shù)完全一樣,但是代碼的結(jié)構(gòu)已經(jīng)清晰了很多,我們把一個(gè)大函數(shù)拆分了 3 個(gè)小函數(shù),去掉了許多嵌套的條件分支語(yǔ)句。
目前已經(jīng)有了不小的進(jìn)步,但我們不會(huì)滿足于此,雖然已經(jīng)把大函數(shù)拆分成了互不影響的 3 個(gè)小函數(shù),但可以看到,請(qǐng)求在鏈條傳遞中的順序非常僵硬,傳遞請(qǐng)求的代碼被耦合在了業(yè)務(wù)函數(shù)之中:
const order500 = function (orderType, pay, stock) { if (orderType === 1 && pay === true) { console.log('500 元定金預(yù)購(gòu), 得到 100 優(yōu)惠券'); } else { order200(orderType, pay, stock); // 將請(qǐng)求傳遞給 200 元訂單 // order200 和 order500 耦合在一起 } };
這依然是違反開(kāi)放—封閉原則的,如果有天我們要增加 300 元預(yù)訂或者去掉 200 元預(yù)訂,意味著就必須改動(dòng)這些業(yè)務(wù)函數(shù)內(nèi)部。就像一根環(huán)環(huán)相扣打了死結(jié)的鏈條,如果要增加、拆除或者移動(dòng)一個(gè)節(jié)點(diǎn),就必須得先砸爛這根鏈條。
4. 靈活可拆分的職責(zé)鏈節(jié)點(diǎn)
本節(jié)我們采用一種更靈活的方式,來(lái)改進(jìn)上面的職責(zé)鏈模式,目標(biāo)是讓鏈中的各個(gè)節(jié)點(diǎn)可以靈活拆分和重組。
首先需要改寫一下分別表示 3 種購(gòu)買模式的節(jié)點(diǎn)函數(shù),我們約定,如果某個(gè)節(jié)點(diǎn)不能處理請(qǐng)求,則返回一個(gè)特定的字符串 'nextSuccessor'
來(lái)表示該請(qǐng)求需要繼續(xù)往后面?zhèn)鬟f:
const order500 = function (orderType, pay, stock) { if (orderType === 1 && pay === true) { console.log('500 元定金預(yù)購(gòu),得到 100 優(yōu)惠券'); } else { return 'nextSuccessor'; // 我不知道下一個(gè)節(jié)點(diǎn)是誰(shuí),反正把請(qǐng)求往后面?zhèn)鬟f } }; const order200 = function (orderType, pay, stock) { if (orderType === 2 && pay === true) { console.log('200 元定金預(yù)購(gòu),得到 50 優(yōu)惠券'); } else { return 'nextSuccessor'; // 我不知道下一個(gè)節(jié)點(diǎn)是誰(shuí),反正把請(qǐng)求往后面?zhèn)鬟f } }; const orderNormal = function (orderType, pay, stock) { if (stock > 0) { console.log('普通購(gòu)買,無(wú)優(yōu)惠券'); } else { console.log('手機(jī)庫(kù)存不足'); } };
接下來(lái)需要把函數(shù)包裝進(jìn)職責(zé)鏈節(jié)點(diǎn),我們定義一個(gè)構(gòu)造函數(shù) Chain
,在 new Chain
的時(shí)候傳 遞的參數(shù)即為需要被包裝的函數(shù),同時(shí)它還擁有一個(gè)實(shí)例屬性 this.successor
,表示在鏈中的下 一個(gè)節(jié)點(diǎn)。
此外 Chain
的 prototype
中還有兩個(gè)函數(shù),它們的作用如下所示:
// Chain.prototype.setNextSuccessor 指定在鏈中的下一個(gè)節(jié)點(diǎn) // Chain.prototype.passRequest 傳遞請(qǐng)求給某個(gè)節(jié)點(diǎn) const Chain = function (fn) { this.fn = fn; this.successor = null; }; Chain.prototype.setNextSuccessor = function (successor) { return this.successor = successor; }; Chain.prototype.passRequest = function () { const ret = this.fn.apply(this, arguments); if (ret === 'nextSuccessor') { return this.successor && this.successor.passRequest.apply(this.successor, arguments); } return ret; };
現(xiàn)在我們把 3 個(gè)訂單函數(shù)分別包裝成職責(zé)鏈的節(jié)點(diǎn):
const chainOrder500 = new Chain(order500); const chainOrder200 = new Chain(order200); const chainOrderNormal = new Chain(orderNormal);
然后指定節(jié)點(diǎn)在職責(zé)鏈中的順序:
chainOrder500.setNextSuccessor(chainOrder200); chainOrder200.setNextSuccessor(chainOrderNormal);
最后把請(qǐng)求傳遞給第一個(gè)節(jié)點(diǎn):
chainOrder500.passRequest(1, true, 500); // 輸出:500 元定金預(yù)購(gòu),得到 100 優(yōu)惠券 chainOrder500.passRequest(2, true, 500); // 輸出:200 元定金預(yù)購(gòu),得到 50 優(yōu)惠券 chainOrder500.passRequest(3, true, 500); // 輸出:普通購(gòu)買,無(wú)優(yōu)惠券 chainOrder500.passRequest(1, false, 0); // 輸出:手機(jī)庫(kù)存不足
通過(guò)改進(jìn),我們可以自由靈活地增加、移除和修改鏈中的節(jié)點(diǎn)順序,假如某天網(wǎng)站運(yùn)營(yíng)人員又想出了支持 300 元定金購(gòu)買,那我們就在該鏈中增加一個(gè)節(jié)點(diǎn)即可:
const order300 = function () { // 具體實(shí)現(xiàn)略 }; const chainOrder300 = new Chain(order300); chainOrder500.setNextSuccessor(chainOrder300); chainOrder300.setNextSuccessor(chainOrder200);
對(duì)于程序員來(lái)說(shuō),我們總是喜歡去改動(dòng)那些相對(duì)容易改動(dòng)的地方,就像改動(dòng)框架的配置文件遠(yuǎn)比改動(dòng)框架的源代碼簡(jiǎn)單得多。在這里完全不用理會(huì)原來(lái)的訂單函數(shù)代碼,我們要做的只是增加一個(gè)節(jié)點(diǎn),然后重新設(shè)置鏈中相關(guān)節(jié)點(diǎn)的順序。
5. 異步的職責(zé)鏈
在上一節(jié)的職責(zé)鏈模式中,我們讓每個(gè)節(jié)點(diǎn)函數(shù)同步返回一個(gè)特定的值"nextSuccessor"
,來(lái)表示是否把請(qǐng)求傳遞給下一個(gè)節(jié)點(diǎn)。而在現(xiàn)實(shí)開(kāi)發(fā)中,我們經(jīng)常會(huì)遇到一些異步的問(wèn)題,比如我們要在節(jié)點(diǎn)函數(shù)中發(fā)起一個(gè) ajax異步請(qǐng)求,異步請(qǐng)求返回的結(jié)果才能決定是否繼續(xù)在職責(zé)鏈中 passRequest
。
這時(shí)候讓節(jié)點(diǎn)函數(shù)同步返回"nextSuccessor"
已經(jīng)沒(méi)有意義了,所以要給 Chain
類再增加一個(gè)原型方法 Chain.prototype.next
,表示手動(dòng)傳遞請(qǐng)求給職責(zé)鏈中的下一個(gè)節(jié)點(diǎn):
Chain.prototype.next = function () { return this.successor && this.successor.passRequest.apply(this.successor, arguments); };
來(lái)看一個(gè)異步職責(zé)鏈的例子:
const fn1 = new Chain(function () { console.log(1); return 'nextSuccessor'; }); const fn2 = new Chain(function () { console.log(2); setTimeout(() => { this.next(); }, 1000); }); const fn3 = new Chain(function () { console.log(3); }); fn1.setNextSuccessor(fn2).setNextSuccessor(fn3); fn1.passRequest();
現(xiàn)在我們得到了一個(gè)特殊的鏈條,請(qǐng)求在鏈中的節(jié)點(diǎn)里傳遞,但節(jié)點(diǎn)有權(quán)利決定什么時(shí)候把請(qǐng)求交給下一個(gè)節(jié)點(diǎn)。可以想象,異步的職責(zé)鏈加上命令模式(把 ajax 請(qǐng)求封裝成命令對(duì)象),我們可以很方便地創(chuàng)建一個(gè)異步 ajax 隊(duì)列庫(kù)。
6. 職責(zé)鏈模式的優(yōu)缺點(diǎn)
前面已經(jīng)說(shuō)過(guò),職責(zé)鏈模式的最大優(yōu)點(diǎn)就是解耦了請(qǐng)求發(fā)送者和 N 個(gè)接收者之間的復(fù)雜關(guān)系,由于不知道鏈中的哪個(gè)節(jié)點(diǎn)可以處理你發(fā)出的請(qǐng)求,所以你只需把請(qǐng)求傳遞給第一個(gè)節(jié)點(diǎn)即可,如下圖所示。
用職責(zé)鏈模式改進(jìn)后:
在手機(jī)商城的例子中,本來(lái)我們要被迫維護(hù)一個(gè)充斥著條件分支語(yǔ)句的巨大的函數(shù),在例子里的購(gòu)買過(guò)程中只打印了一條 log 語(yǔ)句。其實(shí)在現(xiàn)實(shí)開(kāi)發(fā)中,這里要做更多事情,比如根據(jù)訂單種類彈出不同的浮層提示、渲染不同的 UI 節(jié)點(diǎn)、組合不同的參數(shù)發(fā)送給不同的 cgi 等。用了職責(zé)鏈模式之后,每種訂單都有各自的處理函數(shù)而互不影響。
其次,使用了職責(zé)鏈模式之后,鏈中的節(jié)點(diǎn)對(duì)象可以靈活地拆分重組。增加或者刪除一個(gè)節(jié)點(diǎn),或者改變節(jié)點(diǎn)在鏈中的位置都是輕而易舉的事情。這一點(diǎn)我們也已經(jīng)看到,在上面的例子中,增加一種訂單完全不需要改動(dòng)其他訂單函數(shù)中的代碼。
職責(zé)鏈模式還有一個(gè)優(yōu)點(diǎn),那就是可以手動(dòng)指定起始節(jié)點(diǎn),請(qǐng)求并不是非得從鏈中的第一個(gè)節(jié)點(diǎn)開(kāi)始傳遞。比如在公交車的例子中,如果我明確在我前面的第一個(gè)人不是售票員,那我當(dāng)然可以越過(guò)他把公交卡遞給他前面的人,這樣可以減少請(qǐng)求在鏈中的傳遞次數(shù),更快地找到合適的請(qǐng)求接受者。這在普通的條件分支語(yǔ)句下是做不到的,我們沒(méi)有辦法讓請(qǐng)求越過(guò)某一個(gè) if 判斷。
拿代碼來(lái)證明這一點(diǎn),假設(shè)某一天網(wǎng)站中支付過(guò)定金的訂單已經(jīng)全部結(jié)束購(gòu)買流程,我們?cè)诮酉聛?lái)的時(shí)間里只需要處理普通購(gòu)買訂單,所以我們可以直接把請(qǐng)求交給普通購(gòu)買訂單節(jié)點(diǎn):
orderNormal.passRequest(1, false, 500); // 普通購(gòu)買, 無(wú)優(yōu)惠券
如果運(yùn)用得當(dāng),職責(zé)鏈模式可以很好地幫助我們組織代碼,但這種模式也并非沒(méi)有弊端,首先我們不能保證某個(gè)請(qǐng)求一定會(huì)被鏈中的節(jié)點(diǎn)處理。此時(shí)的請(qǐng)求就得不到答復(fù),而是徑直從鏈尾離開(kāi),或者拋出一個(gè)錯(cuò)誤異常。在這種情況下,我們可以在鏈尾增加一個(gè)保底的接受者節(jié)點(diǎn)來(lái)處理這種即將離開(kāi)鏈尾的請(qǐng)求。
另外,職責(zé)鏈模式使得程序中多了一些節(jié)點(diǎn)對(duì)象,可能在某一次的請(qǐng)求傳遞過(guò)程中,大部分節(jié)點(diǎn)并沒(méi)有起到實(shí)質(zhì)性的作用,它們的作用僅僅是讓請(qǐng)求傳遞下去,從性能方面考慮,我們要避免過(guò)長(zhǎng)的職責(zé)鏈帶來(lái)的性能損耗。
7. 用 AOP 實(shí)現(xiàn)職責(zé)鏈
在之前的職責(zé)鏈實(shí)現(xiàn)中,我們利用了一個(gè) Chain
類來(lái)把普通函數(shù)包裝成職責(zé)鏈的節(jié)點(diǎn)。其實(shí)利用 JavaScript 的函數(shù)式特性,有一種更加方便的方法來(lái)創(chuàng)建職責(zé)鏈。
下面我們改寫一下之前的 Function.prototype.after
函數(shù),使得第一個(gè)函數(shù)返回'nextSuccessor'
時(shí),將請(qǐng)求繼續(xù)傳遞給下一個(gè)函數(shù),無(wú)論是返回字符串'nextSuccessor'
或者 false
都只是一個(gè)約定,當(dāng)然在這里我們也可以讓函數(shù)返回 false
表示傳遞請(qǐng)求,選擇'nextSuccessor'
字符串是因?yàn)樗雌饋?lái)更能表達(dá)我們的目的,代碼如下:
Function.prototype.after = function (fn) { const self = this; return function () { const ret = self.apply(this, arguments); if (ret === 'nextSuccessor') { return fn.apply(this, arguments); } return ret; } }; const order = order500yuan.after(order200yuan).after(orderNormal); order(1, true, 500); // 輸出:500 元定金預(yù)購(gòu),得到 100 優(yōu)惠券 order(2, true, 500); // 輸出:200 元定金預(yù)購(gòu),得到 50 優(yōu)惠券 order(1, false, 500); // 輸出:普通購(gòu)買,無(wú)優(yōu)惠券
用 AOP 來(lái)實(shí)現(xiàn)職責(zé)鏈既簡(jiǎn)單又巧妙,但這種把函數(shù)疊在一起的方式,同時(shí)也疊加了函數(shù)的作用域,如果鏈條太長(zhǎng)的話,也會(huì)對(duì)性能有較大的影響。
8. 小結(jié)
在 JavaScript 開(kāi)發(fā)中,職責(zé)鏈模式是最容易被忽視的模式之一。實(shí)際上只要運(yùn)用得當(dāng),職責(zé)鏈模式可以很好地幫助我們管理代碼,降低發(fā)起請(qǐng)求的對(duì)象和處理請(qǐng)求的對(duì)象之間的耦合性。職責(zé)鏈中的節(jié)點(diǎn)數(shù)量和順序是可以自由變化的,我們可以在運(yùn)行時(shí)決定鏈中包含哪些節(jié)點(diǎn)。
無(wú)論是作用域鏈、原型鏈,還是 DOM 節(jié)點(diǎn)中的事件冒泡,我們都能從中找到職責(zé)鏈模式的影子。職責(zé)鏈模式還可以和組合模式結(jié)合在一起,用來(lái)連接部件和父部件,或是提高組合對(duì)象的效率。學(xué)會(huì)使用職責(zé)鏈模式,相信在以后的代碼編寫中,將會(huì)對(duì)你大有裨益。
到此這篇關(guān)于JavaScript設(shè)計(jì)模式之職責(zé)鏈模式詳解的文章就介紹到這了,更多相關(guān)JS職責(zé)鏈模式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript插件化開(kāi)發(fā)教程 (三)
前面我們學(xué)習(xí)了jQuery的方式開(kāi)發(fā)插件,講訴的都是些基礎(chǔ)的理論知識(shí),今天開(kāi)始,我們就來(lái)實(shí)戰(zhàn)一下,學(xué)習(xí)開(kāi)發(fā)自己的插件庫(kù)。2015-01-01layui監(jiān)聽(tīng)工具欄的實(shí)例(操作列表按鈕)
今天小編就為大家分享一篇layui監(jiān)聽(tīng)工具欄的實(shí)例(操作列表按鈕),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09js通過(guò)audioContext實(shí)現(xiàn)3D音效
這篇文章主要為大家詳細(xì)介紹了js通過(guò)audioContext實(shí)現(xiàn)3D音效,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-04-04JavaScript頁(yè)面實(shí)時(shí)顯示當(dāng)前時(shí)間實(shí)例代碼
最近因?yàn)轫?xiàng)目需要,有個(gè)需求是讓實(shí)時(shí)顯示當(dāng)前時(shí)間,然后想想這不簡(jiǎn)單嗎,自己就動(dòng)手敲代碼,但是發(fā)現(xiàn)一個(gè)問(wèn)題,通過(guò)getMonth()得到月份,總是會(huì)比當(dāng)前月份少1,深深覺(jué)得實(shí)踐出真知啊…之前覺(jué)得Date對(duì)象挺簡(jiǎn)單的,有很多細(xì)節(jié)都沒(méi)有注意。下面這篇文章就給大家詳細(xì)介紹下。2016-10-10