[譯]編寫更快窗轩、更好的JavaScript的13個技巧

10年前,亞馬遜分享一個例子座咆,每100毫秒的延遲都會是他們損失1%的銷售收入痢艺,即在全年中,每增加1秒鐘的加載時間將使該公司損失約16億美元介陶。同樣堤舒,谷歌發(fā)現(xiàn)搜索頁面的生成時間增加500毫秒,訪問量將減少20%哺呜,潛在的廣告收入也將減少五分之一舌缤。

我們中很少人可以像谷歌和亞馬遜一樣去處理這種大場面,但是某残,相同的原則也適用于更小規(guī)模的場景国撵,速度更快的代碼可以帶來更好的用戶體驗,并且對業(yè)務(wù)更有利玻墅。特別是在web開發(fā)中介牙,速度可能在與對手競爭中成為關(guān)鍵因素。在較快的網(wǎng)絡(luò)上浪費的每一個毫秒澳厢,就會在較慢的網(wǎng)絡(luò)上放大耻瑟。

在本文中,我們將探討13種提升JavaScript運行速度的實用技巧赏酥,無論你是寫基于Node.js的服務(wù)端代碼還是客戶端的JavaScript代碼。我已經(jīng)提供了基于https://jsperf.com創(chuàng)建的性能測試用例谆构。如果你想自己測試這些技巧裸扶,請確保點擊這些鏈接。

Do It Less

最快的代碼是那些從來不會運行的代碼搬素。

1. 刪除無用的功能

開始著手優(yōu)化已經(jīng)寫好的代碼是一件很容易的事情呵晨,但是魏保,對性能提升最大的方法往往來自于退后一步問問自己為什么我們的代碼需要出現(xiàn)在這里。

在繼續(xù)某項優(yōu)化工作之前摸屠,問問自己你的代碼是否真的需要做他現(xiàn)在所做的事情谓罗。這個功能里面的組件或者函數(shù)是否有必要?如果沒有季二,請刪掉它檩咱。這一步對提升代碼速度非常重要,卻很容易被忽略胯舷。

2. 避免無用的步驟

基準(zhǔn)測試:https://jsperf.com/unnecessary-steps

在較小的規(guī)模上刻蚯,一個函數(shù)運行過程中執(zhí)行的每一步都有用么?舉個例子桑嘶,為了達到最終的效果炊汹,你的數(shù)據(jù)是否會陷于一個沒有必要的圈中?下面的示例可能被簡化了逃顶,但是讨便,它能代表那些在較大代碼量中很難被發(fā)現(xiàn)的問題。

'incorrect'.split('').slice(2).join('');  // converts to an array
'incorrect'.slice(2);                     // remains a string 

即使在簡單的例子中以政,性能上的差異也是十分巨大的霸褒,運行某些代碼比不運行任何代碼要慢得多!盡管很少有人會犯上述錯妙蔗,但是面對更長傲霸、更復(fù)雜的代碼,在獲取結(jié)果的前加上一些毫無價值的步驟就會很容易眉反。盡量避免它昙啄!

Do It Less often

如果你不能刪除代碼,問問你自己能不能減少做這件事情的頻率呢寸五?代碼如此強大的原因之一是他可以使用我們輕松的完成重復(fù)的操作梳凛,但是,也更容易讓我們的代碼執(zhí)行次數(shù)超過需要的次數(shù)梳杏。以下是一些需要注意的特殊情況韧拒。

3. 越早退出循環(huán)越好

基準(zhǔn)測試:https://jsperf.com/break-loops/1

在一個循環(huán)中找出不需要迭代完成的情況。舉個例子十性,如果你正在尋找一個特殊值并且已經(jīng)找到他了叛溢,那么剩下的迭代就已經(jīng)不需要了。你應(yīng)該通過使用break語句來中斷正在執(zhí)行中的循環(huán):

for (let i = 0; i < haystack.length; i++) {
    if (haystack[i] === needle) {
        break;
    }
}

或者劲适,如果楷掉,你需要只對循環(huán)中某些元素做操作時,你可以使用continue語句來跳過對其他元素進行操作霞势。continue會終止當(dāng)前迭代中的執(zhí)行語句烹植,立即跳轉(zhuǎn)到下一個語句中:

for (let i = 0; i < haystack.length; i++) {
    if (!haystack[i] === needle) {
        continue;
    }
    doSomething();
}

值得注意的是斑鸦,你也可以通過breakcontinue跳過嵌套的循環(huán):

loop1: for (let i = 0; i < haystacks.length; i++) {
    loop2: for (let j = 0; j < haystacks[i].length; j++) {
        if (haystacks[i][j] === needle) {
            break loop1;
        }
    }
}

4. 僅僅初始化一次

基準(zhǔn)測試:https://jsperf.com/pre-compute-once-only/6 (譯者在mac下自測,使用/不使用閉包草雕,使用/不使用全局變量巷屿,目前對性能影響差異不大)

在我們的應(yīng)用中,我們將會調(diào)用數(shù)次下列方法:

function whichSideOfTheForce1(name) {
    const light = ['Luke', 'Obi-Wan', 'Yoda'];
    const dark = ['Vader', 'Palpatine'];

    return light.includes(name) ? 'light' : dark.includes(name) ? 'dark' : 'unknown';
}

whichSideOfTheForce1('Luke');
whichSideOfTheForce1('Vader');
whichSideOfTheForce1('Anakin');

這段代碼墩虹,我們每次調(diào)用whichSideOfTheForce1時嘱巾,都會重新創(chuàng)建2次數(shù)組,每次調(diào)用時都需要給我們的數(shù)組重新分配內(nèi)存败晴。

提供的數(shù)組的值是固定的浓冒,那最好的解決辦法就是定義一次,然后在函數(shù)中調(diào)用它的引用尖坤。盡管我們也可以全局定義這2個數(shù)組變量稳懒,但是,這將允許他們在我們的函數(shù)外部被篡改慢味。最好的解決方法是使用閉包场梆,這就意味著他返回的是一個函數(shù):

function whichSideOfTheForceClosure1(name) {
    const light = ['Luke', 'Obi-Wan', 'Yoda'];
    const dark = ['Vader', 'Palpatine'];

    return (name) => (light.includes(name) ? 'light' : dark.includes(name) ? 'dark' : 'unknown');
}
const whichSideOfTheForce2 = whichSideOfTheForceClosure1();

現(xiàn)在,我們的數(shù)組只會初始化一次了纯路。再來看看下面的例子:

function doSomething(arg1, arg2) {
    function doSomethingElse(arg) {
        return process(arg);
    };
    return doSomethingElse(arg1) + doSomethingElse(arg2);
}

每次運行doSomething時或油,都會從頭開始創(chuàng)建嵌套函數(shù)doSomethingElse。 閉包提供了解決方案驰唬, 如果我們返回一個函數(shù)顶岸,doSomethingElse仍然是私有的,但只會創(chuàng)建一次:

function doSomething(arg1, arg2) {
    function doSomethingElse(arg) {
        return process(arg);
    };
    return (arg1, arg2) => doSomethingElse(arg1) + doSomethingElse(arg2);
}

5. 控制代碼的執(zhí)行順序來保證最小的運行次數(shù)

基準(zhǔn)測試: https://jsperf.com/choosing-the-best-order/1

如果仔細(xì)考慮函數(shù)中每一步的執(zhí)行順序叫编,也可以幫助我們提高代碼的執(zhí)行效率辖佣。假設(shè),我們有一個數(shù)組來存儲上平的價格(美分)搓逾,我們需要一個函數(shù)對商品的價格進行求和并返回結(jié)果(美元):

const cents = [2305, 4150, 5725, 2544, 1900];

這個函數(shù)有2件事情要做卷谈,轉(zhuǎn)化單位和求和,但是這些動作的順序很重要霞篡。如果優(yōu)先處理轉(zhuǎn)化單位世蔗,我們函數(shù)是這樣的:

function sumCents(array) {
    return '$' + array.map(el => el / 100).reduce((x, y) => x + y);
}

在這個方法中,我們對數(shù)組的每一項都需要進行除法朗兵,如果改變執(zhí)行的順序污淋,我們只需要進行一次除法:

function sumCents(array) {
    return '$' + array.reduce((x, y) => x + y) / 100;
}

優(yōu)化性能的關(guān)鍵就是確保函數(shù)以最佳的順序執(zhí)行。

6. 了解代碼的時間復(fù)雜度 O(n)

了解代碼的時間復(fù)雜度是理解為什么某些方法比其他方法運行的更快余掖,占用的內(nèi)存更少的最佳方法之一寸爆。例如,你可以通過使用時間復(fù)雜一目了然的了解為什么二分搜索是效率最好的搜索算法之一,為什么快排是往往是最有效的排序算法而昨。詳細(xì)請自行了解時間復(fù)雜度

Do It Faster

代碼速度優(yōu)化收益最大的往往是前面2類。在本節(jié)中我們將討論提高代碼速度的幾種方法找田,他們更多的是和代碼優(yōu)化相關(guān)歌憨,而不是剔除他或者減少運行的次數(shù)。

當(dāng)然墩衙,這些優(yōu)化也要減少代碼的大小或者使其對編譯器更友好务嫡,但是,表面上看你只更改了代碼而已漆改。

7. 多用內(nèi)置函數(shù)

基準(zhǔn)測試:https://jsperf.com/prefer-built-in-methods/1

對于擁有編譯器和底層語言經(jīng)驗的人來說心铃,這是一件很明顯的事情。但是挫剑,這里還是要把它作為一個基礎(chǔ)規(guī)則來提一下去扣,如果JavaScript有內(nèi)置函數(shù),請使用它樊破。

編譯器代碼在設(shè)計時愉棱,就針對方法或者對象類型進行了性能優(yōu)化。另外哲戚,內(nèi)置方法的底層語言是C++奔滑。除非你的用例特別具體,否則顺少,你自己的JavaScript代碼很少能比現(xiàn)有內(nèi)置代碼快朋其。

為了測試這個,我們自己來實現(xiàn)一個map方法

function map(arr, func) {
    const mapArr = [];
    for (let i = 0; i < arr.length; i++) {
        const result = func(arr[i], i, arr);
        mapArr.push(result);
    }
    return mapArr;
}

讓我們來創(chuàng)建一個數(shù)組脆炎,里面包含了100個隨機數(shù)字(1-100)梅猿。

const arr = [...Array(100)].map(e=>~~(Math.random()*100));

我們來執(zhí)行一些簡單操作(數(shù)字乘2)來比較二者的差異:

map(arr, el => el * 2);  // Our JavaScript implementation
arr.map(el => el * 2);   // The built-in map method

在我的測試中,我們自己實現(xiàn)的map方法比原生的Array.prototype.map慢65%腕窥。

8. 選擇最佳的數(shù)據(jù)類型

基準(zhǔn)測試1:set.add()vs array.push() https://jsperf.com/adding-to-a-set-vs-pushing-to-an-array

基準(zhǔn)測試2:map.set() vs object['xx'] https://jsperf.com/adding-map-vs-adding-object

同樣粒没,最佳的性能也可能來自于選擇合適的內(nèi)置數(shù)據(jù)類型。JavaScript中內(nèi)置的數(shù)據(jù)類型遠(yuǎn)遠(yuǎn)不止:Number簇爆、String癞松、FunctionObject入蛆。很多不常見的數(shù)據(jù)類型如果在正確的場景中使用將會提供非常明顯的優(yōu)勢响蓉。

SetMap在頻繁添加和刪除元素的情況下有明顯的性能優(yōu)勢。

了解內(nèi)置的對象類型哨毁,并嘗試使用最適合你需要的對象類型枫甲,這對提升代碼的性能非常有用。

9. 別忘了內(nèi)存

JavaScript作為一種高級語言,它為你處理很多底層細(xì)節(jié)想幻。內(nèi)存管理就是其中一個粱栖。JavaScript使用一種稱為垃圾回收(GC)的系統(tǒng)來釋放內(nèi)存,在不需要開發(fā)人員明確指示的情況下脏毯,就可以自動釋放內(nèi)存闹究。

盡管內(nèi)存管理在JavaScript中是自動的,但這并不意味著它是完美的食店。你也可以采取其他步驟來管理內(nèi)存并減少內(nèi)存泄漏的機會渣淤。

例如,SetMap有變體WeakSetWeakMap吉嫩,他們持有對象的“弱”引用价认。他們通過確保其中的對象沒有其他對象引用時觸發(fā)垃圾回收,來確保不會出現(xiàn)內(nèi)存泄漏自娩。

在ES2017之后用踩,你可以通過TypedArray對象來更好的控制內(nèi)存的分配。例如椒功,Int8Array可以放-128到127之間的值捶箱,僅僅占用一個字節(jié)。但是动漾,值得注意的是丁屎,使用TypedArray的性能提升可能很小:將常規(guī)數(shù)組與Uint32Array進行比較寫入性能略有改善旱眯,但讀取性能卻幾乎沒有改善晨川。

對于底層編程語言有基本的了解可以幫助你編寫更快、更好的JavaScript代碼删豺。

10. 盡可能使用單態(tài)

基準(zhǔn)測試1:https://jsperf.com/monomorphic-forms

基準(zhǔn)測試2:https://jsperf.com/impact-of-function-arguments

如果我們設(shè)置const a = 2共虑,則變量a可以被視為多態(tài)的(可以更改)。 相反呀页,如果我們直接使用2妈拌,則可以認(rèn)為是單態(tài)的(其值是固定的)。

當(dāng)然蓬蝶,如果我們需要多次使用變量尘分,則設(shè)置變量非常有用。 但是丸氛,如果你只使用一次變量培愁,則完全避免設(shè)置變量會稍快一些。 采取簡單的乘法功能:

// 函數(shù)定義
function multiply(x, y) {
    return x * y;
}

如果我們運行multiply(2, 3)缓窜,他比直接運行下面的代碼快1%:

// 定義2個變量作為multiply的參數(shù)
let x = 2, y = 3;
multiply(x, y);

這是一個小勝利定续,在大型代碼中谍咆,性能提升往往是由大量小勝利組成的。

同樣私股,在函數(shù)中使用參數(shù)可提供靈活性摹察,但會降低性能。 如果不需要它們倡鲸,就可以把它變成一個常量放在函數(shù)中港粱,它會略微提高性能。因此旦签,multiply的更快版本如下所示:

// 如果3是固定不變的時候,則直接作為函數(shù)中的一部分
function multiplyBy3(x) {
    return x * 3;
}

結(jié)合上述優(yōu)化寸宏,在我的測試中性能提升約為2%宁炫。雖然改動的點比較小,但是如果可以在大型代碼庫中多次進行這種改進氮凝,就值得考慮了羔巢。

譯者注,這里原文太繞了罩阵,大家看看代碼里面的注釋理解一下

a. 僅在值必須是動態(tài)的時才引入函數(shù)參數(shù)竿秆,否則,就寫成函數(shù)內(nèi)部的變量稿壁;

b. 僅在多次使用某一個值時才引入變量幽钢,否則,就直接寫值傅是;

11. 避免使用delete

基準(zhǔn)測試1: https://jsperf.com/removing-variables-from-an-object/1

基準(zhǔn)測試2: https://jsperf.com/delete-vs-map-prototype-delete

delete關(guān)鍵詞的作用是用來刪除對象中的某一個屬性匪燕。也許你會覺得這個對于你的應(yīng)用來說很有用,但是喧笔,希望你盡量別去用它帽驯。在v8引擎中,delete關(guān)鍵詞消除了hidden class的優(yōu)勢书闸,讓對象變成了一個"慢對象"尼变。

hidden class:由于 JavaScript 是一種動態(tài)編程語言,屬性可進行動態(tài)的添加和刪除浆劲,這意味著一個對象的屬性是可變的嫌术,大多數(shù)的 JavaScript 引擎(V8)為了跟蹤對象和變量的類型引入了隱藏類的概念。在運行時 V8 會創(chuàng)建隱藏類梳侨,這些類附加到每個對象上蛉威,以跟蹤其形狀/布局。這樣可以優(yōu)化屬性訪問時間

根據(jù)你的需求走哺,可能僅僅將不需要的屬性設(shè)置成undefined就夠了蚯嫌。

const obj = { a: 1, b: 2, c: 3 };
obj.a = undefined;

我在網(wǎng)上看過一些建議哲虾,他們使用以下的功能去拷貝除去指定屬性之外的對象:

const obj = { a: 1, b: 2, c: 3 };
const omit = (prop, { [prop]: _, ...rest }) => rest;
const newObj = omit('a', obj);

但是,在我的測試中择示,上面的函數(shù)比delete關(guān)鍵詞還要慢束凑。另外,它的可讀性也很低栅盲。

或者汪诉,你可以考慮使用Map而不是Object,因為谈秫,Map.prototype.deletedelete也快很多扒寄。

Do It Later

如果你做不到上述3個方面的優(yōu)化,你也可以試一試第四類優(yōu)化拟烫,即使運行時間完全相同也會讓你覺代碼更快该编。這涉及重構(gòu)代碼,使整體性較小或要求較高的任務(wù)不會阻塞最重要的代碼執(zhí)行硕淑。

12. 使用異步代碼避免線程堵塞

默認(rèn)情況下课竣,JavaScript是單線程的,并且會同步的執(zhí)行代碼置媳。(實際上于樟,瀏覽器代碼可能正在運行多個線程來捕獲事件并觸發(fā)處理程序,但就編寫JavaScript代碼而言拇囊,它是單線程的)

同步執(zhí)行對大多是JavaScript代碼都適用迂曲,但是,如果我們需要執(zhí)行的代碼需要很長時間寥袭,但是昔逗,我們又不想堵塞其他更重要的代碼執(zhí)行晌缘。

我們就需要使用異步代碼肴捉。像fetch()或者XMLHttpRequest()這些內(nèi)置方法強制是異步執(zhí)行的核蘸。值得注意的是,任何同步函數(shù)都可以異步化:如果你在執(zhí)行耗時的同步操作尝江,例如對大型數(shù)組中的每個項目執(zhí)行操作涉波,則可以使此代碼異步化,這樣就不會阻止其他代碼的執(zhí)行炭序。

此外啤覆,在NodeJs中,很多模塊都存在同步方法和異步方法兩種惭聂,例如窗声,fs.writeFile()fs.writeFileSync()。在正常情況下辜纲,請默認(rèn)使用異步方法笨觅。

13. 使用Code Split

如果你在瀏覽器中寫JavaScript拦耐,那么你應(yīng)該優(yōu)先確保你的頁面展示的越快越好〖#“首屏渲染”是一個衡量瀏覽器渲染第一個有效界面時間的關(guān)鍵指標(biāo)杀糯。

改善此問題的最佳方法就是通過JavaScript代碼拆分。與其將所有代碼都打包在一起苍苞,不如將其拆分成較小的塊固翰,這樣就可以預(yù)加載更少的JavaScript代碼。根據(jù)你是用的引擎不同羹呵,代碼拆分的方法也不同骂际。

Tree-Shaking是一個從代碼庫中剔除無用的代碼的策略,你可以通過這篇文章tree-shaking來了解他冈欢。

總結(jié)

確保你的優(yōu)化策略有效的最佳方案就是測試他們方援,我在文章中使用測試性能,你也可以試一試:

Chrome的開發(fā)者工具中的性能和網(wǎng)絡(luò)面板是檢查web應(yīng)用的性能的好工具涛癌,同時,我還推薦使用Google的LightHouse送火。

最后拳话,盡管速度很重要,但是种吸,速度并不是好代碼的全部弃衍。可讀性和可維護性也很重要坚俗,如果為了提升輕微的性能而導(dǎo)致需要花更多的時間去找BUG并修復(fù)它镜盯,事情將變得很不值得。

關(guān)于我

我是一個莫得感情的代碼搬運工猖败,每周會更新1至2篇前端相關(guān)的文章速缆,有興趣的老鐵可以掃描下面的二維碼關(guān)注或者直接微信搜索前端補習(xí)班關(guān)注。

image

精通前端很難恩闻,讓我們來一起補補課吧艺糜!
好啦,翻譯完畢啦幢尚,原文鏈接在此 13 Tips to Write Faster, Better-Optimized JavaScript破停。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市尉剩,隨后出現(xiàn)的幾起案子真慢,更是在濱河造成了極大的恐慌,老刑警劉巖理茎,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件黑界,死亡現(xiàn)場離奇詭異管嬉,居然都是意外死亡,警方通過查閱死者的電腦和手機园爷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門宠蚂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人童社,你說我怎么就攤上這事求厕。” “怎么了扰楼?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵呀癣,是天一觀的道長。 經(jīng)常有香客問我弦赖,道長项栏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任蹬竖,我火速辦了婚禮沼沈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘币厕。我一直安慰自己列另,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布旦装。 她就那樣靜靜地躺著页衙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪阴绢。 梳的紋絲不亂的頭發(fā)上店乐,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機與錄音呻袭,去河邊找鬼眨八。 笑死,一個胖子當(dāng)著我的面吹牛左电,可吹牛的內(nèi)容都是我干的踪古。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼券腔,長吁一口氣:“原來是場噩夢啊……” “哼伏穆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起纷纫,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤枕扫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后辱魁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烟瞧,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡诗鸭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了参滴。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片强岸。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖砾赔,靈堂內(nèi)的尸體忽然破棺而出蝌箍,到底是詐尸還是另有隱情,我是刑警寧澤暴心,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布妓盲,位于F島的核電站,受9級特大地震影響专普,放射性物質(zhì)發(fā)生泄漏悯衬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一檀夹、第九天 我趴在偏房一處隱蔽的房頂上張望筋粗。 院中可真熱鬧,春花似錦炸渡、人聲如沸娜亿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至促脉,卻和暖如春辰斋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瘸味。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工宫仗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人旁仿。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓藕夫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親枯冈。 傳聞我的和親對象是個殘疾皇子毅贮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,828評論 2 345