编程镓教・入门篇|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阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容