開啟JavaScript函數(shù)式編程

碼字辛苦昭伸,個人原創(chuàng),轉(zhuǎn)載請注明作者及出處澎迎。謝謝合作庐杨!

本文描述了 JavaScript 函數(shù)式編程的若干重要特征,以及一些好的實踐建議夹供。意在引導以前是非函數(shù)式編程的同學灵份,能快速切入到函數(shù)式編程的理念中來;而對于正在“函數(shù)式”的同學哮洽,也可鞏固認識填渠,同時也希望提出意見交流。

另外,本文略長氛什,只消了解 ES6 莺葫,就無閱讀困難,請讀者耐心閱讀枪眉。

背景介紹

關(guān)于函數(shù)式編程的起源捺檬,有一段這樣“不接地氣”的歷史。

在眾多光芒萬丈的一群人之中贸铜,有一位叫阿隆佐堡纬。他設(shè)計了一個名為 lambda 演算的形式系統(tǒng)。這個系統(tǒng)實質(zhì)上是為其中一個超級機器設(shè)計的編程語言蒿秦。在這種語言里面烤镐,函數(shù)的參數(shù)是函數(shù),返回值也是函數(shù)棍鳖。

除了阿隆佐·邱奇炮叶,艾倫·圖靈也在進行類似的研究。他設(shè)計了一種完全不同的系統(tǒng)(后來被稱為 圖靈機)鹊杖,并用這種系統(tǒng)得出了和阿隆佐相似的答案悴灵。到了后來人們證明了圖靈機和 lambda 演算的能力是一樣的扛芽。

由于二戰(zhàn)的推動骂蓖,1949 年,現(xiàn)實世界中率先誕生了第一臺圖靈機川尖,相比之下登下,運行阿隆佐的 lambda 演算硬件(Lisp 機)到了1973 年才得以實現(xiàn),這還得歸功于 MIT叮喳。

引用這段歷史說明說明什么呢被芳?說明函數(shù)式編程很有來頭。

一個簡單易懂的模型

我們的程序本質(zhì)上都可以描述為:輸入數(shù)據(jù) => 運算處理 => 輸出數(shù)據(jù)

I => {... f ...} => O
  • 輸入數(shù)據(jù)并不復雜馍悟,只要給它一定的結(jié)構(gòu)就好了
  • 輸出數(shù)據(jù)也不復雜畔濒,因為復雜的數(shù)據(jù)并不是我們想要的

所以兩端簡單,中間很復雜锣咒。所有這些復雜的過程都交給函數(shù) f侵状。如果一個函數(shù) f 太過膨脹,或者無法勝任毅整,那就用 n 個函數(shù)來分擔解決趣兄。

I => {... f1 => f2 => f3 => ... => fn ...} => O

對于上述模型,請讀者只消專注于函數(shù)及函數(shù)彼此的關(guān)系悼嫉,并將數(shù)據(jù)屏蔽在視線之外艇潭。

其他編程風格

函數(shù)式編程和語言無關(guān),它只不過是一種編程風格(再直白點就是一種思維方式,一種代碼組織習慣)而已蹋凝。只要有函數(shù)的語言鲁纠,幾乎都能進行函數(shù)式編程。只不過有些語言鳍寂,天生就能做到更純粹而已房交。

作為編程風格,我們常見的還有以下這些伐割。

1候味、面向?qū)ο缶幊蹋∣OP)
面向?qū)ο髲娬{(diào)數(shù)據(jù)與行為綁定。

2隔心、命令式編程(CP)
數(shù)據(jù)與行為深度耦合白群。

3、聲明式編程(DP?)
我們常見的 SQL 數(shù)據(jù)庫操作語言硬霍,便是聲明式編程的典范帜慢。這篇 文章 已經(jīng)講的足夠清楚了。

4唯卖、函數(shù)式編程(FP)
數(shù)據(jù)和行為是解耦的粱玲。函數(shù)式編程屬于聲明式編程。

5拜轨、圖形化編程(GP?)
MIT 的 Scratch 是一款典型的圖形化編程語言抽减。

到此,我們?nèi)匀粺o需理會上面提到的種種概念橄碾。等 JSer 們刷完新聞卵沉,沖上了一杯咖啡,才開始言歸正傳法牲。

函數(shù)式編程的關(guān)鍵特征

首先史汗,函數(shù)式編程是不是 “燒腦” 編程?對我們普羅大眾來說拒垃,或許還輪不到 “燒腦”停撞,要燒也是那些可敬的布道師們幫我們頂替了。

也就是說函數(shù)式編程似難也不難悼瓮,那該如何學習函數(shù)式編程呢戈毒?

在筆者看來,仍然可以采用 “黑盒子” 學習方法谤牡,我們先從它的一些關(guān)鍵特征入手副硅,而有意的屏蔽一些底層而復雜的知識。

純函數(shù)

純函數(shù)是函數(shù)式編程的第一重要特征翅萤。它有兩條原則:

  • 對于相同的輸入恐疲,一定有相同的輸出腊满;
  • 對輸入的東西,不要改變它培己,對引用的東西碳蛋,也不要改變它。

第一條好說省咨,第二條就是所謂的無 “副作用”肃弟。

我們常常所寫的不純的函數(shù),基本上都是副作用滿天飛。比如下面的 “副作用” 的例子。

let arr = [1, 2, 3, 4, 5, 6];

// slice 是一個純函數(shù)
arr.slice(0, 3);
// =>[1, 2, 3]

arr.slice(0, 3);
// =>[1, 2, 3]

// splice 是一個不純的函數(shù)
arr.splice(0, 3);
// => [1, 2, 3]

arr.splice(0, 3);
// => [4, 5, 6]

上述示例淀弹,slice 函數(shù)只要輸入是 (0, 3) 無論執(zhí)行多少次,返回值恒為 [1, 2, 3];
但是 splice 函數(shù)相同的輸入箩兽,執(zhí)行 2 遍,返回的值就不同了章喉。原因是 splice 每次執(zhí)行汗贫,額外的改變了(破壞了)數(shù)組 arr 。這就是副作用秸脱。

再看一個副作用的例子:

let temperature = 35;
function check(t) {
    // 副作用1
    return t > temperature;
}
function monitor(day) {
    // 副作用2
    if(check(day.temperature)){
        console.warn('High temperature warning!');
    }
}

短短的幾行代碼落包,就有 2 處副作用。

副作用 1 因為依賴了外部的系統(tǒng)變量 temperature, 一旦別處導致這個系統(tǒng)變量變化(這是難以說清的事)摊唇,那么這個 check 函數(shù)就不滿足相同輸入恒有相同輸出了咐蝇。

副作用 2 盡管 monitor 滿足相同輸入恒輸出 undefined, 但它仍然依賴了外部變量 check 函數(shù),仍然可能有未知事情發(fā)生遏片。

副作用帶給我們的麻煩是很多的嘹害,除了每次得小心翼翼撮竿,更為麻煩的事是吮便,一旦系統(tǒng)變量改變,因為跨度太大幢踏,問題將很難定位髓需。

如何消除 “副作用”,其實非常容易:

const TEMPERATURE = 35;
function check(t) {
    // 最好的做法是將變量 TEMPERATURE 收入函數(shù)體保護起來
    return t > TEMPERATURE;
}
function monitor(check, day) {
    if(check(day.temperature)){
        console.warn('High temperature warning!');
    }
}

減少副作用房蝉,其實不僅是函數(shù)式編程的要求僚匆,在我們?nèi)粘>幊讨幸矐撆囵B(yǎng)這樣的代碼習慣。很多優(yōu)秀的技術(shù)框架也在遵循著這一原則搭幻。

Redux 技術(shù)思想就提倡無副作用的純函數(shù)咧擂,這點從 reducer 的設(shè)計就體現(xiàn)出來了。當然檀蹋,React 本身也包含很多函數(shù)式編程思想松申,在此就不去展開了。

一些 I/O 是天生自帶副作用的,正如上文所提到的贸桶,這部分我們有一些特殊的處理辦法舅逸。JavaScript 天然存在而且還相當隱晦的副作用就是 this,下文會介紹到它皇筛。除此之外琉历,JavaScript 很多的副作用都是可以避免的,關(guān)鍵是培養(yǎng)好避免副作用的習慣水醋。

柯里化 curry

柯里化的主要思路:
“函數(shù)接收多個參數(shù)旗笔,一次調(diào)用" 轉(zhuǎn)變成 "函數(shù)每次只接收一個參數(shù),分多次調(diào)用”拄踪。

簡言之换团,就是將多維變成一維。

curry:: f(x1, x2, ...xn) =  f(x1)(x2)(...xn)

用具體函數(shù)舉例就很容易理解了宫蛆。

// 柯里化之前
let distance = function(x, y, z){
    return Math.sqrt(x*x + y*y + z*z);
}
distance(1, 4, 8);
// => 9

// 柯里化之后
let distance_curried = function(x){
    return function(y){
        return function(z){
            return Math.sqrt(x*x + y*y + z*z);
        }
    }
}
// 分多次調(diào)用
var xDistance = distance_curried(1);
var xyDistance = xDistance(4);
var myDistance = xyDistance(8);
// => 9

// 簡寫為
distance_curried(1)(4)(8)
// => 9

柯里化一個函數(shù)的結(jié)果艘包,就是新生成的函數(shù),每次傳一個參數(shù)耀盗,執(zhí)行后返回的仍是一個函數(shù)想虎,直至返回最后結(jié)果。

換言之叛拷,函數(shù)每次只接收一個參數(shù)舌厨,執(zhí)行后,就返回一個新函數(shù)處理剩余的參數(shù)忿薇。

至于柯里化算法怎么實現(xiàn)的裙椭,這里不去追究。正如前文介紹的署浩,函數(shù)式編程是一種聲明式編程揉燃,只管做什么,不管怎么做筋栋。因此炊汤,只需知道柯里化做的是分多次調(diào)用,但不管它是怎么做到的弊攘。

約定:函數(shù)在前抢腐,數(shù)據(jù)在后

這是一條重要約定。約定了作為參數(shù)時襟交,函數(shù)們在前迈倍,數(shù)據(jù)在最后。

首先捣域,它強調(diào)了函數(shù)的地位啼染,準確的說是我們編程習慣中的地位——函數(shù)應該站在前排醋界。

其次,數(shù)據(jù)是我們最后考慮的東西提完,我們始終關(guān)注 “映射邏輯” 本身的建設(shè)形纺。

再次,約定這樣的參數(shù)順序徒欣,某些函數(shù)經(jīng)柯里化之后逐样,不至于會搞不清楚本次調(diào)用是該傳函數(shù)還是該傳數(shù)據(jù)。

從下面的示例打肝,來看看我們?nèi)绾稳プ裱@條重要約定脂新。

// 1、將數(shù)組 filter 方法封裝一下
let arrFilter = function(f, arr) {
    return arr.filter(f); 
}

// 2粗梭、柯里化
let filter = curry(arrFilter);

//結(jié)束争便,就這么簡單

// 第一次調(diào)用
let filterSpaces = filter(hasSpaces);
//插一個問題:請問 hasSpaces 是個啥?

// 對断医,回答它是個函數(shù)滞乙,一定是沒錯的
// 因為函數(shù)式編程的世界全是函數(shù)嘛~
let hasSpaces = (val) => /\s+/g.test(val);

// 第二次調(diào)用
filterSpaces(['jeremy', 'jere my'])
// => ['jere my']

函數(shù)式編程的世界遍地都是函數(shù),尤其是一個函數(shù)柯里化后鉴嗤,幾乎絕大部分函數(shù)的執(zhí)行結(jié)果斩启,仍然是一個函數(shù)。

這仍然可以尋跡阿隆佐當時提出的 “在這種語言里面醉锅,函數(shù)的參數(shù)是函數(shù)兔簇,返回值也是函數(shù)”

所以硬耍,忘掉煩惱吧垄琐,忘掉與副作用糾纏打斗的記憶吧,現(xiàn)在滿地都是白花花经柴、金燦燦的函數(shù)狸窘。

在函數(shù)的海洋遨游吧-侵刪.jpg

組合

兩個函數(shù)組合之后返回了一個新函數(shù)。就這么簡單口锭!

var fnC = compose(fnA, fnB);

組合 (compose) 是函數(shù)式編程的一個重要概念朦前,有了它,就可以任意 “擺布” 函數(shù)了鹃操。

var first = (x) => x[0];
var reverse = reduce((acc, x) => [x].concat(acc), []); 

// 組合后生成一個新函數(shù)
var last = compose(first, reverse);

// 新函數(shù)開始吃進數(shù)據(jù)
last(['jeremy', 'hello', 'world']);
// => 'world'

// 要是反過來組合
var reverse_one = compose(reverse, first);

// 新函數(shù)開始吃進相同數(shù)據(jù)
reverse_one(['jeremy', 'hello', 'world']);
// => Uncaught TypeError: reverse is not a function

可見,組合內(nèi)的參數(shù)順序不能隨意置換和顛倒春哨。

組合滿足結(jié)合律

組合中處理的全是函數(shù)荆隘,且 compose 中作為參數(shù)的函數(shù),是從右往左依次調(diào)用赴背,即最靠后的函數(shù)被優(yōu)先執(zhí)行(先進后出)椰拒。

compose(f, compose(g, h))
依次從右向左調(diào)用晶渠,即 h() -> g() -> f()

由此組合的結(jié)合律是:
compose(f, compose(g, h)) == compose(compose(f , g), h)

組合的結(jié)合律是相鄰參數(shù)兩兩組合,并沒有顛倒參數(shù)順序燃观。

注意褒脯,Ramda.js 的 R.pipe 則是從左往右執(zhí)行函數(shù)組合(先進先出),但這是另外一碼事缆毁。

組合也有好的實踐

讓組合可重用度高就是好的組合實踐番川。

結(jié)合律的一大好處是任何一個函數(shù)分組都可以被拆開來,然后再以它們自己的組合方式兩兩組合在一起脊框。

compose(addSymbol, toUpperCase, first, reverse)

拆解 & 組合 1:

var last = compose(first, reverse);
var symboledUpperLast = compose(addSymbol, toUpperCase, last);

拆解 & 組合 2:

var last = compose(first, reverse);
var upperLast = compose(toUpperCase, last);
var symboledUpperLast = compose(addSymbol, upperLast);

拆解 & 組合 3:

var last = compose(first, reverse);
var symboledUpper = compose(addSymbol, toUpperCase);
var symboledUpperLast = compose(symboledUpper, last);

誰的可重用性高颁督,感覺是第 3 種,也說不準浇雹,得有更多的實際需求沉御,才能判斷這事。

范疇學

范疇學 是組合的理論依據(jù)昭灵。它和集合論吠裆,函數(shù)理論都有很多相關(guān)概念。概念本身也不難理解烂完,此處不贅敘硫痰。

其它特征 - pointfree

函數(shù)無須提及將要操作的數(shù)據(jù)是什么樣的。

阮大大的文章講解得非常細致窜护。其實 pointfree 也不是什么復雜的概念效斑,運用一等公民函數(shù)、柯里化(curry)以及組合這些武器柱徙,就很容易實現(xiàn)這個目標缓屠。

敲黑板強調(diào) - 全部都是函數(shù)

如果每一個函數(shù)都是一個兵,那全城皆兵护侮。草木仍然是草木敌完,草木...呃,是數(shù)據(jù)羊初。

無論柯里化(curry)滨溉,還是組合(compose),都是面向于函數(shù)长赞,最后生成一個函數(shù)晦攒,任何時候,你見到的幾乎都是函數(shù)得哆,函數(shù)時刻待命脯颜。

let stylity = compose(map(addSymbol), reverse); 

其實本條不算是特征,到算作一條反復洗腦的 “碎碎念”贩据。addSymbol 是一個函數(shù)栋操,map(addSymbol) 運算后是一個函數(shù)闸餐,最后的結(jié)果 stylity 仍然是一個函數(shù)。

函數(shù)式編程的一些好的實踐

這些好的實踐矾芙,并不是函數(shù)式編程所專有的舍沙,但是有助于加深對函數(shù)式編程風格的理解。同時剔宪,它們應該貫穿在我們設(shè)計拂铡、代碼之中。實踐得多了歼跟,我們也就更容易過渡到函數(shù)式編程和媳。

等價替換

var hi = function(name){ return "Hi " + name; }; 
var greeting = function(name) { 
    return hi(name); 
};
// 等價
greeting = hi;

因為函數(shù)是純的,不會有副作用哈街。那么接收相同的輸入留瞳,返回相同的輸出,兩個函數(shù)就是等價的骚秦。

既然等價她倘,為啥還要多一層裹腳布?所以直接賦值相等即可作箍。
但在布滿地雷的非函數(shù)式編程中硬梁,不純的函數(shù),等價替換往往需要很慎重胞得。

“包裹它”不如“暴露它”

包裹一個函數(shù)荧止,不如直接把它暴露成參數(shù)。因為這符合強調(diào)函數(shù)地位的要求阶剑。

$.get('/path/fp', function(json){
    return renderGet(json); 
});

以上是一個常用 ajax 的運用跃巡。更為常見的要求是,如果有報錯牧愁,那得增加一個 error 參數(shù)素邪,我們繼續(xù)參考 nodejs 將錯誤參數(shù)放在第一個參數(shù)位置的約定,做出以下調(diào)整:

$.get('/path/fp', function(error, json){
    return renderGet(error, json); 
});

這是自然想到的修改方案猪半,但是也面臨著還得修改 renderGet 函數(shù)的麻煩兔朦,如果有多處這樣使用,那得多處修改磨确。

如果沽甥,僅僅遵循一條原則(養(yǎng)成思維習慣就好了)——突出函數(shù)的地位,增加函數(shù)的曝光度俐填,那就會有這樣的修改思路:

$.get('/path/fp', renderGet);

這樣的好處是安接,無論要求 renderGet 函數(shù)修改改成什么樣的參數(shù)形式,都只限制在這個函數(shù)本身了英融。

順便提一下的是盏檐,一些 API 設(shè)計中,在設(shè)計傳參數(shù)時驶悟,指明傳遞一般參數(shù)胡野,不如指明傳遞一個函數(shù)。

解耦函數(shù)痕鳍,函數(shù)名稱請通用化

寫業(yè)務(wù)邏輯時硫豆,有些中間函數(shù)或者輔助會被提取出來,此時的命名一般會和業(yè)務(wù)耦合笼呆。等到相關(guān)代碼都寫完后熊响,或者你在做 codeview 時,你會發(fā)現(xiàn)它和業(yè)務(wù)其實是可以解耦的诗赌。那么當時的那種基于業(yè)務(wù)上下文思考的函數(shù)命名汗茄,就完全可以改成一般化的命名,讓它從名字上看就顯得是通用的铭若。

在命名的時候洪碳,我們特別容易把自己限定在特定的數(shù)據(jù)上。這種現(xiàn)象很常見叼屠,也是重復造輪子的一大原因瞳腌。

函數(shù)式編程更多的專注在函數(shù)身上,它有著比較徹底的函數(shù)與數(shù)據(jù)解耦镜雨,所以壓根不會有這么強的數(shù)據(jù)耦合嫂侍。但這一條實踐,也值得我們一般式編程借鑒荚坞。

避免 this 的副作用

let Sound = {
    _sound: 'miao',
    play() {
        console.log(this._sound);
    }
}

上面是一個非常常見的示例挑宠,如果遵循了函數(shù)是一等公民包裹它不如暴露它 等等這些理念或建議西剥,那么在需要的時候痹栖, play 方法就應該被當作另一個函數(shù)的參數(shù)。比如:

$.ajaxSuccess(Sound.paly);

因為 Sound.paly 函數(shù)中使用了 this瞭空,而它指向了函數(shù)外部即調(diào)用上下文揪阿。從純函數(shù)定義的角度看,this 就是一塊最大的 “副作用"咆畏。

解決的辦法大家都知道南捂,就是將 this 鎖在籠子里,如同將權(quán)力之手鎖在籠子里一樣旧找。

$.ajaxSuccess(Sound.play.bind(this));

而事實上溺健,但在函數(shù)式編程中根本用不到它。

結(jié)語

說了這么多钮蛛,關(guān)于函數(shù)式編程鞭缭,以上最重要的兩點就是:

  • 函數(shù)是一等公民剖膳,要時刻把函數(shù)放在參數(shù)位置
  • 每一個函數(shù)盡量是無副作用的純函數(shù)

至于那些底層的、高級的岭辣、數(shù)學的邏輯吱晒,就把它們統(tǒng)統(tǒng)先關(guān)在 “黑盒子” 里吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沦童,一起剝皮案震驚了整個濱河市仑濒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌偷遗,老刑警劉巖墩瞳,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異氏豌,居然都是意外死亡喉酌,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門箩溃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瞭吃,“玉大人,你說我怎么就攤上這事涣旨⊥峒埽” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵霹陡,是天一觀的道長和蚪。 經(jīng)常有香客問我,道長烹棉,這世上最難降的妖魔是什么攒霹? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮浆洗,結(jié)果婚禮上催束,老公的妹妹穿的比我還像新娘。我一直安慰自己伏社,他們只是感情好抠刺,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著摘昌,像睡著了一般速妖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上聪黎,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天罕容,我揣著相機與錄音,去河邊找鬼。 笑死锦秒,一個胖子當著我的面吹牛露泊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播脂崔,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼滤淳,長吁一口氣:“原來是場噩夢啊……” “哼梧喷!你這毒婦竟也來了砌左?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤铺敌,失蹤者是張志新(化名)和其女友劉穎汇歹,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體偿凭,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡产弹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了弯囊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片痰哨。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖匾嘱,靈堂內(nèi)的尸體忽然破棺而出斤斧,到底是詐尸還是另有隱情,我是刑警寧澤霎烙,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布撬讽,位于F島的核電站,受9級特大地震影響悬垃,放射性物質(zhì)發(fā)生泄漏游昼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一尝蠕、第九天 我趴在偏房一處隱蔽的房頂上張望烘豌。 院中可真熱鬧,春花似錦看彼、人聲如沸廊佩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽罐寨。三九已至,卻和暖如春序矩,著一層夾襖步出監(jiān)牢的瞬間鸯绿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瓶蝴,地道東北人毒返。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像舷手,于是被迫代替她去往敵國和親拧簸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容