編程鎵教?入門篇|004 一切為了變化

學習目標

  • 理解函數、參數、聲明和調用的概念
  • 掌握用變量和函數應對變化的方法

學習用時:60分鐘

通過前幾課的學習,我們已經掌握了用代碼來畫點、線和方塊的方法,現在大家都能在畫布上畫出自己喜歡的圖案了。

接下來,我們希望讓畫面能夠動起來。比如說,讓你畫出的小人從畫面的一邊,移動到另一邊去。

但是,怎樣才能把我們畫出的圖案,移動到另一個位置呢?

工匠的困境

在很久很久以前,有一位法老想要給自己建造一座雕像。于是他找來了一位手藝精湛的工匠,并選好了一座山頭。于是工匠拿來錘子和鑿子,叮叮當當地開工了……

image.png

就這樣日復一日、年復一年;不知不覺間,五十年過去了……

image.png

工匠終于完成了雕像,此時他已是個白發蒼蒼的老人。他把幾乎一生的心血都花在了這座雕像上,看著自己完成的作品,心中感到無比地自豪和驕傲。然而,法老在視察完他的工作成果后,冒出輕描淡寫的一句話來,讓他頓時萬念俱灰:

“挺好的,就是有點歪。往右邊挪上一米吧!”

上面這個故事來源于《The Art of Readable Code》一書。這個故事生動地闡釋了下面這條原則:

Change Is The Only Constant:唯一不變的就是變化

image.png

根據默菲定律,任何可能發生的事情,只要給足夠的時間,就一定會發生。所以我們在編程時,要盡可能地考慮到需求變化的可能性。否則需求一旦發生變化,就會不可避免地陷入到工匠的困境中去。

移動一下試試看

image.png

請在Chrome瀏覽器中打開下面的鏈接:

http://codepen.io/zhangshenjia/pen/ZKbWEz

網頁加載完成之后,應該能看到這樣的界面:

image.png

在這個程序中,我們畫好了一個十字,像不像射擊游戲里的準星?這是一個僅由五個點組成,簡單得不能再簡單的圖形了。

如果我們想把這個準星向右挪動一個像素,該怎么修改程序呢?

我們首先想到的是,應該把所有點的水平坐標都加一,也就是把所有畫點語句中的第一個數字加一。讓我們來試試看:

image.png

到現在為止,你感覺還OK吧?那是因為這個圖案只有五個點。那如果我讓你把上節課的作業里的圖案移動一下呢?

現在,我們正面臨著和工匠一樣的困境:由于在我們用代碼畫出的圖案里,每一個點的坐標都是固定的數字。因此如果想要移動圖案,哪怕只有一個像素的距離,都必須修改所有畫點語句中的坐標。如果我們的圖案由成百上千的點組成,那全部修改一遍簡直就是個噩夢!

還記得上節課我們學過DRY原則(Don’t Repeat Yourself)嗎?有沒有覺得這樣的修改很重復呢?要是能夠只修改一個地方,就自動同步到所有使用的地方就好了……

用變量來適應變化

不妨先想一想,在生活中遇到會變化的需求時,我們是怎么處理的呢?

image.png

首先,我們得設計一個可以變化的組件,并通過它來對變化進行適應。比如汽車座椅中可以調節角度的軸承、活動扳手中的可以調節卡口尺寸的蝸輪。

那在編程中,有沒有可以這樣變化的東西呢?當然有,那就是上節課我們就用過的變量

首先刷新一下頁面,把代碼復原。然后在第一句畫點代碼上方添加一個空行,輸入下面的代碼:

var x = 0;

image.png

這樣我們就定義了一個變量 x用來保存水平的坐標,并給它賦值為 0。接下來,我們把所有畫點代碼中第一個數字前面都加上 x +

image.png

注意:在符號 + 左右各有一個空格。它們雖然沒有實際意義,但能使我們代碼顯得更清晰、讀起來更省力。對此感興趣的同學可以課后自行搜索一下“代碼風格”。

現在我們可以修改 var x = 0 中的初始值看看,比方說改成 5

image.png

Oh Yeah,我們只修改了一行代碼,就可以讓整個圖案進行水平移動了!

接下來,我們可以用同樣的方法來實現圖案的垂直移動。定義一個變量 y ,并在所有畫點代碼中的第二個數字前面都加上 y +

image.png

這樣一來,我們就可以通過修改變量 xy 的值,把圖案移動到畫布的任何位置。再也不怕修改位置的需求了!

想要更多怎么辦?

image.png

要知道,需求的任何一部分都可能發生變化。除了圖案所處的位置之外,圖案的數量也會可能會變。現在畫布上只有一個十字,要是我們需要畫更多的十字怎么辦?

有同學說:這好辦,只要把畫十字的代碼再復制一份,然后修改 xy 的值就可以了嘛!想畫多少十字,就復制多少次唄!

image.png

那如果我們要畫100個十字該怎么辦,把代碼復制100次嗎?我們畫十字的這段代碼只有短短幾行,多復制幾遍貌似還可以接受。但如果我們畫的是一個復雜的圖案,需要幾百行代碼來完成呢?

復制代碼確實可以簡單粗暴地臨時解決問題,但事后修改起來就很麻煩了。比如說,我們想把畫面上所有的十字都改成紅色,就需要在所有復制出來的代碼里都加上一行更換顏色的代碼。萬一改完發現還是黑色好看的話,還得把剛才添加的代碼一行行刪掉……

需求只發生了一個很小的變動,就要修改一大堆重復代碼,業內把這樣的情況稱之為“霰彈式修改”。由于我們是人不是機器,這樣做很累自不用說,在做大量修改時也難免會發生疏忽,比如漏加了一處代碼,又或者在刪除換顏色代碼時錯把畫點代碼刪掉……

啥?我都寫了這么多WORK了,你告訴我你其實要的是WORD?

又有同學說:那我們能不能用上節課學過的循環呢?把畫十字的代碼放在循環體里,然后每次循環改變 xy 的值不就行了嗎?這樣畫十字的代碼就只會出現一次了呀!

問題是,循環只能用來處理連續性的重復工作,對非連續的重復無能為力。我們可以用循環來一次性畫出N個十字,但是不能中途停下來。然而,有很多重復性的工作都不是連續性的。

比如在某個網絡游戲中,獲取經驗值有很多方法(殺死敵人、完成任務、掛機……),經驗值滿了之后就需要升級,然后提升人物的一系列屬性,還有可能學得新的技能。那么在獲取經驗值之后,判斷是否需要升級的邏輯就需要多次重復運行,但獲取經驗值的邏輯卻散落在程序中多個不同的地方……這樣的需求,是無法通過循環來解決的。

那除了循環之外,還有什么辦法可以讓一段代碼能夠重復使用呢?答案就是:函數

什么是函數?

打醬油去!

想象一下,如果你家沒有醬油了,需要去超市買,但你自己又不想跑腿,正好孩子放學回家,就想讓他去打醬油。因為孩子之前沒干過這事,所以你得教他具體該怎么做。

「打醬油」的流程:

  • 帶上足夠的錢,出門去超市
  • 找到調味品區,拿一瓶醬油
  • 在收銀臺結帳,收好找零
  • 把醬油拿回家,交到你手上

這樣一來,以后再需要買醬油的時候,只要告訴孩子“打醬油去”就行了,而不用再把整個流程重新講一遍了。( 你說啥,都忘光了?那我再給你講一遍……)

「打醬油」就是一個函數,同時它也是這個函數的函數名。而打醬油的具體流程,就是這個函數的函數體

函數(Function):可以在程序內被重復調用的一段代碼
函數名(Function Name):函數對外的名稱
函數體(Function Statement):函數內部執行的具體流程

教孩子怎么打醬油,就是在聲明這個函數。對孩子說“打醬油去”,就是在調用這個函數。而孩子最后交到你手上的醬油,就是函數的返回值

聲明(Declare):告知程序的執行者有這么一個函數存在
調用(Call):在程序運行的過程中,要求執行某個函數
返回值(Return Value):函數調用完畢后的返回結果

顯然,如果你從來沒有教過孩子,就讓他去打醬油,他肯定會蒙圈的。一個函數必須得先經過聲明,才能進行調用。因為如果不進行聲明,程序的執行者根本不知道有這個函數存在,當然也就無法去執行了。

函數的返回值并不是必須提供的。有的函數要求提供一個明確的返回值,比如「買醬油」這個函數,就明確要求拿到一瓶醬油,即便因為各種原因沒有買到,那也得給出個說法;而有的函數則只看重運行的過程,比如「冥想」這個函數,并不需要最后拿出個什么成果來。

image.png

函數可以使一段邏輯在不同地方被重復調用。可以用函數來解決那些循環無法解決的非連續性重復問題。由于每個調用的地方只會出現函數名,而不會出現具體的邏輯。這樣在需求發生變化時,不管這個函數被調用了多少次,我們都只需要修改函數體里的邏輯就行。

當然,更改函數名的時候,所有調用這個函數的地方還是不可避免地要同步修改。所以起一個好名字,非常非常地重要!關于怎么給函數起一個好名字來盡量避免修改,同學們可以在課后搜索一下。

需求有變化怎么辦?

image.png

不過,這樣的函數雖然解決了在不同地方重復調用的問題,但每次執行的邏輯都是固定不變的。比如「打醬油」函數,在不出意外(超市關門、沒貨……)的情況下,每次都會得到一瓶醬油。

然而我們知道:需求是不可能一成不變的。今天我們需要一瓶醬油,明天可能要十個饅頭,后天則可能要一打可樂……要怎樣才讓函數可以應對這些變化呢?

首先想到的是,我們能不能給購買每種商品的流程都聲明一個函數,并在需要的時候調用它們呢?就像這樣:「買饅頭」、「買可樂」……

這樣雖然貌似解決了問題,卻產生了一大堆邏輯雷同的函數。如果購買流程中的任一環節的邏輯變更,就需要同步修改所有的函數。何況即便是相同的商品,每次買的數量也可能不同,難道還要聲明「打醬油」、 「打2瓶醬油」、「打3瓶醬油」……這樣一系列的函數嗎?

image.png

我們可以把函數調整修改一下,來應對可能發生的變化:

「買東西」的流程:(調用時需要說明要買的「東西」及「數量」)

  • 帶上足夠的錢,出門去超市
  • 找到貨架,拿「數量」的「東西」
  • 在收銀臺結帳,收好找零
  • 拿回家,交到你手上

「買東西」也是一個函數。但和「打醬油」有所不同的是,在調用「買東西」時需要指明「數量」「東西」,它們都是函數的參數

參數(Arguments):調用函數時所提供的數據

在函數體內,可以用與參數同名的變量,來訪問傳入的數據。假設我們在調用「買東西」函數時傳入的「數量」3「東西」是** 辣條,那么函數的第二步實際執行的流程是這樣的:“找到貨架,拿三包辣條”

參數不一定都是必須提供的,提供了默認值的參數可以省略。有的參數是必須提供的,比如要買的「東西」,如果不說清楚,就根本不知道要買啥;而有的參數是可以省略的,比如要買的「數量」,在沒有提供的情況下,那就默認只買一份。

image.png

通過更換傳入的參數,我們不需要對函數內部邏輯進行改動,就能控制邏輯的變化。比如,我們可以發起這樣調用:「買兩包鹽」、「買五瓶啤酒」……

用函數來畫十字

接下來,我們要聲明一個「畫十字」的函數,在調用時把坐標當成參數傳進去,這樣就可以在畫布的任意坐標位置畫出十字了。如果想畫多個十字的話,多調用幾次就行了。

首先,我們把剛才添加那兩行 var 語句刪掉,替換成下面的代碼:

function drawCross(x, y) {

然后,在最后一句畫點語句后面增加一個空行,輸入一個符號 }

image.png

這樣我們就聲明了一個函數,名為 drawCross (draw是“畫”,cross是“十字”,聯合起來就是“畫十字”的意思)。這個函數有兩個參數:xy,指定了十字在水平和垂直兩個方向上的位置坐標。在函數體內會自動聲明兩個和參數同名的對應變量 xy,它們只能在函數體內部使用。

需要注意的是,函數名里是不允許有空格的。像drawCross這樣把多個單詞直接連起來,并讓首字母大寫的方法叫做駝峰命名法。也有draw_cross這樣的命名法,不過還是駝峰命名法比較常用。雖然我們也可以直接用中文「畫十字」來當函數名,但我強烈建議不要這么做。

現在的函數體沒有縮進,看起來結構不清晰。讓我們選中函數體里所有的畫點代碼,按下 TAB 鍵增加縮進,這樣代碼看起來就舒服多了:

image.png

但是現在畫布是空的,我們的十字到哪里去了呢?原來我們只聲明了函數,并沒有調用它,所以函數體里的邏輯并不會被執行。接下來,就讓我們添加一個函數調用吧。

image.png

在程序的最底部添加一個空行,輸入下面的代碼:

drawCross(0, 0);

image.png

十字出現了!在程序執行到我們剛剛添加的這一句時,就會跳轉到 drawCross 函數內部去執行,執行完后再回來繼續往下走。就像我們讀外文書時,發現一個不認識的單詞就停下來去查字典,查完回來接著讀一樣。

現在我們可以通過繼續調用這個函數,在畫面的不同位置畫出更多的十字了。試著在程序底部添加這幾行代碼:

drawCross(0, 3);
drawCross(3, 0);
drawCross(3, 3);

image.png

我們通過四個十字組合出了一個符號 #,顯然這是個更復雜的圖案。那如果我們想要把這個圖案移動到其他位置,該怎么做呢?

在函數里調用函數

這個圖案是通過對 drawCross 函數進行四次調用畫出來的。那么我們直接修改這四行代碼里的調用參數行不行呢?

image.png

當然可以!畢竟要修改的只有四行代碼,但要是我們的圖案是由100個十字組成的呢?那要修改多少行代碼?

我們再一次遭遇了工匠的困境,那我們是不是還可以用變量來隔離變化呢?

image.png

當然可以!不過,如果我們需要畫多個符號 # 呢?還是得復制一堆代碼……這樣一下霰彈式修改還是無法避免。那么,我們能不能像聲明 drawCross 函數來畫十字一樣,再聲明一個 drawHash 函數來畫符號 # 呢?

image.png

當然可以!要知道函數體是一段代碼,而函數的調用也是一行代碼。所以我們可以在函數體里再調用別的函數,就可以我們在循環體內使用循環一樣。

image.png

那能不能在函數里聲明函數呢?當然也可以,但這樣聲明出來新函數只能在舊函數里使用。關于函數作用域的內容,感興趣的同學可以課后搜索一下。

我們在四句對 drawCross 函數的調用前面加上一句代碼:

function drawHash(x, y) {

然后在程序最后面加上一個 },這樣就定義了一個 drawHash 函數。不要忘記給函數體縮進噢:

image.png

現在圖案消失了,因為我們還沒有添加調用呢。隨便給個坐標,調用一下看看吧:

image.png

為什么圖案還是畫在左上角,沒有畫在我們指定的坐標呢?因為在 drawHash 函數里對 drawCross 函數進行調用時,并沒有把我們指定的坐標傳遞過去。雖然這兩個函數里都有 xy 這兩個參數,各自函數體里都有同名的兩個變量,但是它們互相是沒有關系的。

image.png

我們在調用 drawHash 函數時使用的參數是 10, 10,所以在 drawHash 函數的變量 xy 的值都是 10。但在調用 drawCross 函數時的參數就不一樣了,比如第二次調用時的參數是 0, 3 ,那在 drawCross 函數內的變量 xy 的值就分別為 0, 3

每個函數的參數變量都只能在函數內部使用,外部是無法訪問的,只能通過調用時傳入參數來對其進行賦值。關于變量作用域的內容,感興趣的同學可以課后搜索一下。

如果想讓我們給 drawHash 函數傳遞的參數影響 drawCross 函數,就得在調用 drawCross 函數時改變參數,也就是把 xy 加進去:

image.png

大功告成!現在我們有了 drawCrossdrawHash 兩個函數,可以用一行代碼畫出十字,也可以用一行代碼畫出#。當然,你總是可以在現有函數的基礎上,構造出更復雜的函數……最終,你就可以僅僅用一行代碼,就畫出一個很復雜的圖案來。

能不能在drawHash函數里再調用drawHash函數自己呢?理論上是可以的,這種做法叫做遞歸(Recursion)。遞歸是一種比較有難度的編程技巧,需要精心設計控制流程,避免發生無限調用。現在我們還用不著它,感興趣的同學可以課后搜索一下。

「自底而上」vs「自頂向下」

到目前為止,我們做了下面這些事:

  • 先想辦法畫一個點
  • 用同樣的方法畫一堆點來組成圖案
  • 把這一堆畫點的代碼聲明為一個函數
  • 通過調用函數和畫點,畫出更復雜的圖案
  • 把這一堆畫圖的代碼再聲明為一個函數
  • ……

這種“先看看能做點什么,然后再看看能做點別的什么”的思考和行動模式,我們稱之為自底而上(Bottom-up)。每走一步就能看到對應變化,一步一個腳印,走得很踏實。

image.png

然而,在解決實際問題時,僅僅靠「自底而上」是不行的。因為能做的事情實在太多了,但可能絕大多數都和我們現在想做的事情沒什么關系。只著眼于當下能做什么,而不思考我們想做什么,就可能會迷失方向,一直在原地踏步;甚至于南轅北轍,離目標越來越遠……

另外一種思路是,先確定好要達成的目標,制定一個整體規劃,再分解成具體的行動計劃并執行。這正是我們之前學過的萬金油思路——「拆分」。這種“先想清楚要做什么,然后再看看怎么去做”的模式,我們稱之為自頂向下(Top-down)

image.png

當然,僅僅靠「自頂向下」也是不行的。我們想做的很多事情,現在是做不到的。總是紙上談兵,想太多不切實際的東西,只會浪費時間。結合使用「自底而上」和「自頂向下」這兩種模式,理論聯合實際才是王道。

在用「自頂向下」的思路來分解目標,作出初步的規劃設想的同時;也需要根據目前具備的資源和能力,用「自底而上」的思路來檢驗設想的可行性。只有當我們在這兩種思路之間找到了結合點,才能將設想進一步細化成計劃進而執行。

當設想不可行時,是放棄目標或降低標準,還是去獲取現在不具備的資源和能力呢?這得看目標的優先級有多高、是否是核心需求,在達成目標的期望價值和獲取資源能力的代價中反復做權衡……這已經遠遠超出了本教程的范疇,容我不再細表。

寫一個畫笑臉的函數

假設我們現在的目標是:在畫布上畫出一個笑臉。由于這是一個獨立且完整的任務,所以我們可以聲明一個 drawFace 函數來完成它:

image.png

先用「自底而上」的思路分析:我們已經具備了在畫布的任何位置用任何顏色畫出像素的能力,而畫布上的笑臉肯定是由一堆像素構成的,所以這個目標必然是可達成的。 所以,盡管此時我們的函數里一行代碼都沒有,但我們完全可以相信,這個函數的功能是可以實現的。

所以,這個函數也沒必要現在就寫,可以先去做更重要或更緊迫的事;依賴這個函數的工作(比如寫一個畫小人的函數drawPerson)現在就可以同步開展,而不必非得等到這個函數完成后再進行。只要在必須在畫布上看到笑臉時,把它完成就好。

隨后我們可以用「自頂向下」的思路來分解這個函數。一般來說,一個笑臉由眼睛、嘴、鼻子、眉毛等部分組成。其中眼睛和嘴巴是必需的,所以我們可以再添加兩個函數 drawEyedrawMouth,其余非必須的部分可以先寫成注釋,以后有時間再添加:

image.png

基于同樣的原因,我們斷定 drawEyedrawMouth 函數是可以實現的。所以這時盡管這兩個函數現在還是空的,我們也可以宣告 drawFace 函數完成了,因為它已經完成了自己的使命:羅列所有必要的組成部分,并整理好它們之間的關系。

當然,眼睛和嘴巴之間的距離可能還需要不斷調整,但這無關緊要。至于眼睛和嘴巴到底畫了沒有,畫得怎么樣,我們在驗收 drawFace 函數時并不關心。因為那是 drawEyedrawMouth 函數要完成的任務。

接下來的任務就是完成 drawEyedrawMouth 函數了。我們可以找時間分別來完成它們,也可以分配給別人來干。為簡單起見,我們只畫一個點來當眼睛,畫四個點來當嘴巴:

image.png

笑臉完成了!

函數的價值和意義

“工欲善其事,必先利其器” —— 《論語?衛靈公》

在「我的世界」這款游戲里,玩家一開始手里空空如也,什么都沒有。只能赤手空拳去擼樹,然后拿到木頭做成斧頭等工具,再去高效率地采集更多的資源。

image.png

寫函數的過程,就是打造工具的過程。雖然寫函數的過程比較吃力,但寫出來的函數可以大大地方便我們之后的工作。雖然函數內部的代碼邏輯會比直接堆代碼要復雜一點點,但在調用函數時的代碼卻簡潔了許多。這和解魔方一樣,用初級方法會比較簡單易懂,但步數要多一些;而用高級方法會比較復雜,但步數會少一些。

image.png

當一段邏輯需要多次使用時,簡單地復制粘貼一遍代碼貌似是第一時間就能想到的方法。要是需求稍有變化,那就做一點適當的改動。結果可能就會產生一大堆雷同或者大同小異的代碼:

image.png

我們把一段需要多次使用的邏輯封裝成函數后再調用,顯著地減少了重復代碼。從而避免了直接復制代碼可能導致的“霰彈式修改”,可以更好的適應需求的不斷變化。關于這一點,我們已經通過上面的實踐得到了深刻的體會。

image.png

函數隱藏了不必要的實現細節,同時降低了在修改代碼的過程中出錯的可能性。以機械表為例,如果不用表盤遮住內部,就會給使用者帶來不必要的心理壓力,也很容易損壞其內部精密的結構。

image.png

我們還通過函數名傳達了邏輯意圖 ,使本來需要注釋的代碼意圖變得更直觀,更容易理解。用術語來說,就是提升了代碼的可讀性。很明顯,面對一堆畫點語句,你不看注釋或者不手動運行測試一下,根本不可能明白它畫的是什么。而對一個名為drawCross的函數進行調用,則明明白白地告訴了讀者這行代碼的作用:我要畫一個十字

image.png

最重要的一點是,我們通過函數隔離出了一個抽象層次。這使我們可以將當前的思維局限在某個環節之中,將全部的注意力用于在當前層次上進行完整自洽的思考上。于是我們得以自頂向下地進行框架式思考,將一個復雜的任務不斷地拆分到可以在單位時間內完成的粒度,并最終逐步完成。

image.png

內容回顧

image.png

函數(Function):可以在程序內被重復調用的一段代碼
函數名(Function Name):函數對外的名稱
函數體(Function Statement):函數內部執行的具體流程
聲明(Declare):告知程序的執行者有這么一個函數存在
調用(Call):在程序運行的過程中,要求執行某個函數
返回值(Return Value):函數調用完畢后的返回結果
參數(Arguments):調用函數時所提供的數據

課后作業

image.png

在Chrome中打開下面的地址:

http://codepen.io/zhangshenjia/pen/MmyreE

這里已經寫好了兩個函數 drawPointdrawBox,分別實現了畫點和畫長方形的功能,請先體驗一下它們的威力。

1、用「自頂向下」的方式來實現一個函數,畫出自己喜歡的圖案。你可能需要基于 drawPointdrawBox,聲明更多的自定義函數,并組合使用它們;

2、在每個函數的聲明之前增加一行注釋來說明函數的作用(可參考已有的兩個函數),除此之外盡量少寫或不寫注釋,在函數命名上多下功夫,讓代碼簡明易懂。

有的同學可能會疑惑,為什么函數聲明可以放在函數調用的下面?程序不是按從上向下的順序執行代碼的嗎?執行到函數調用那一行時,函數還沒聲明不是嗎?這個是因為JS獨有的提升(Hoisting)機制,感興趣的同學可以課后搜索一下。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容