高阶函数--实现AOP,函数节流,分时函数,惰性加载函数

高阶函数是指至少满足下列条件之一的函数。

  • 函数可以作为参数被传递;

  • 函数可以作为返回值输出。

  • JavaScript语言中的函数显然满足高阶函数的条件,在实际开发中,无论是将函数当作参数传递,还是让函数的执行结果返回另外一个函数,这两种情形都有很多应用场景,下面就列举一些高阶函数的应用场景。

函数作为参数传递

把函数当作参数传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分。其中一个重要应用场景就是常见的回调函数。

1. 回调函数

在ajax异步请求的应用中,回调函数的使用非常频繁。当我们想在ajax请求返回之后做一些事情,但又并不知道请求返回的确切时间时,最常见的方案就是把callback函数当作参数传入发起ajax请求的方法中,待请求完成之后执行callback函数:

var getUserInfo = function( userId, callback ){
     $.ajax( 'http://xxx.com/getUserInfo?' + userId, function( data ){
        if ( typeof callback === 'function' ){
            callback( data );
        }
    });
}

getUserInfo( 13157, function( data ){
    alert ( data.userName );
});

回调函数的应用不仅只在异步请求中,当一个函数不适合执行一些请求时,我们也可以把这些请求封装成一个函数,并把它作为参数传递给另外一个函数,“委托”给另外一个函数来执行。

比如,我们想在页面中创建100个div节点,然后把这些div节点都设置为隐藏。下面是一种编写代码的方式:

var appendDiv = function(){
    for ( var i = 0; i < 100; i++ ){
        var div = document.createElement( 'div' );
        div.innerHTML = i;
        document.body.appendChild( div );
        div.style.display = 'none';
    }
};

appendDiv();

把div.style.display = 'none'的逻辑硬编码在appendDiv里显然是不合理的,appendDiv未免有点个性化,成为了一个难以复用的函数,并不是每个人创建了节点之后就希望它们立刻被隐藏。

于是我们把div.style.display = 'none'这行代码抽出来,用回调函数的形式传入appendDiv方法:

var appendDiv = function( callback ){
    for ( var i = 0; i < 100; i++ ){
        var div = document.createElement( 'div' );
        div.innerHTML = i;
        document.body.appendChild( div );
        if ( typeof callback === 'function' ){
            callback( div );
        }
    }
};

appendDiv(function( node ){
    node.style.display = 'none';
});

可以看到,隐藏节点的请求实际上是由客户发起的,但是客户并不知道节点什么时候会创建好,于是把隐藏节点的逻辑放在回调函数中,“委托”给appendDiv方法。appendDiv方法当然知道节点什么时候创建好,所以在节点创建好的时候,appendDiv会执行之前客户传入的回调函数。

2. Array.prototype.sort

Array.prototype.sort接受一个函数当作参数,这个函数里面封装了数组元素的排序规则。从Array.prototype.sort的使用可以看到,我们的目的是对数组进行排序,这是不变的部分;而使用什么规则去排序,则是可变的部分。把可变的部分封装在函数参数里,动态传入Array.prototype.sort,使Array.prototype.sort方法成为了一个非常灵活的方法,代码如下:

//从小到大排列

[ 1, 4, 3 ].sort( function( a, b ){
    return a - b;
});

// 输出: [ 1, 3, 4 ]


//从大到小排列

[ 1, 4, 3 ].sort( function( a, b ){
    return b - a;
});

// 输出: [ 4, 3, 1 ]

函数作为返回值输出

相比把函数当作参数传递,函数当作返回值输出的应用场景也许更多,也更能体现函数式编程的巧妙。让函数继续返回一个可执行的函数,意味着运算过程是可延续的。

1. 判断数据的类型

我们来看看这个例子,判断一个数据是否是数组,在以往的实现中,可以基于鸭子类型的概念来判断,比如判断这个数据有没有length属性,有没有sort方法或者slice方法等。但更好的方式是用Object.prototype.toString来计算。Object.prototype.toString.call( obj )返回一个字符串,比如Object.prototype.toString.call( [1,2,3] )总是返回"[object Array]",而Object.prototype.toString.call( "str")总是返回"[object String]"。所以我们可以编写一系列的isType函数。代码如下:

var isString = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object String]';
};

var isArray = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object Array]';
};

var isNumber = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object Number]';
};

我们发现,这些函数的大部分实现都是相同的,不同的只是Object.prototype.toString.call( obj )返回的字符串。为了避免多余的代码,我们尝试把这些字符串作为参数提前值入isType函数。代码如下:

var isType = function( type ){
    return function( obj ){
        return Object.prototype.toString.call( obj ) === '[object '+ type +']';
    }
};

var isString = isType( 'String' );
var isArray = isType( 'Array' );
var isNumber = isType( 'Number' );

console.log( isArray( [ 1, 2, 3 ] ) );     // 输出:true

我们还可以用循环语句,来批量注册这些isType函数:

var Type = {};

for ( var i = 0, type; type = [ 'String', 'Array', 'Number' ][ i++ ]; ){
    (function( type ){
        Type[ 'is' + type ] = function( obj ){
            return Object.prototype.toString.call( obj ) === '[object '+ type +']';
           }
       })( type )
};

Type.isArray( [] );     // 输出:true
Type.isString( "str" );     // 输出:true

2. getSingle

下面是一个单例模式的例子,在第三部分设计模式的学习中,我们将进行更深入的讲解,这里暂且只了解其代码实现:

var getSingle = function ( fn ) {
    var ret;
    return function () {
        return ret || ( ret = fn.apply( this, arguments ) );
    };
};

这个高阶函数的例子,既把函数当作参数传递,又让函数执行后返回了另外一个函数。我们可以看看getSingle函数的效果:

var getScript = getSingle(function(){
    return document.createElement( 'script' );
});

var script1 = getScript();
var script2 = getScript();

alert ( script1 === script2 );    // 输出:true

高阶函数实现AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。

在Java语言中,可以通过反射和动态代理机制来实现AOP技术。而在JavaScript这种动态语言中,AOP的实现更加简单,这是JavaScript与生俱来的能力。

通常,在JavaScript中实现AOP,都是指把一个函数“动态织入”到另外一个函数之中,具体的实现技术有很多,这里我们通过扩展Function.prototype来做到这一点。代码如下:

Function.prototype.before = function( beforefn ){
    var __self = this;    // 保存原函数的引用
    return function(){    // 返回包含了原函数和新函数的"代理"函数
        beforefn.apply( this, arguments );     // 执行新函数,修正this
        return __self.apply( this, arguments );    // 执行原函数
    }
};

Function.prototype.after = function( afterfn ){
    var __self = this;
    return function(){
        var ret = __self.apply( this, arguments );
        afterfn.apply( this, arguments );
        return ret;
    }
};

var func = function(){
    console.log( 2 );
};

func = func.before(function(){
    console.log( 1 );
}).after(function(){
    console.log( 3 );
});

func();

我们把负责打印数字1和打印数字3的两个函数通过AOP的方式动态植入func函数。通过执行上面的代码,我们看到控制台顺利地返回了执行结果1、2、3。

这种使用AOP的方式来给函数添加职责,也是JavaScript语言中一种非常特别和巧妙的装饰者模式实现。这种装饰者模式在实际开发中非常有用。

高阶函数的其他应用

1. currying

首先我们讨论的是函数柯里化(function currying)。currying的概念最早由俄国数学家Moses Schönfinkel发明,而后由著名的数理逻辑学家Haskell Curry将其丰富和发展,currying由此得名。

currying又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

从字面上理解currying并不太容易,我们来看下面的例子。

假设我们要编写一个计算每月开销的函数。在每天结束之前,我们都要记录今天花掉了多少钱。代码如下:

var monthlyCost = 0;

var cost = function( money ){
    monthlyCost += money;
};

cost( 100 );    // 第1天开销
cost( 200 );    // 第2天开销
cost( 300 );    // 第3天开销
//cost( 700 );    // 第30天开销

alert ( monthlyCost );      // 输出:600

通过这段代码可以看到,每天结束后我们都会记录并计算到今天为止花掉的钱。但我们其实并不太关心每天花掉了多少钱,而只想知道到月底的时候会花掉多少钱。也就是说,实际上只需要在月底计算一次。

如果在每个月的前29天,我们都只是保存好当天的开销,直到第30天才进行求值计算,这样就达到了我们的要求。虽然下面的cost函数还不是一个currying函数的完整实现,但有助于我们了解其思想:

var cost = (function(){
    var args = [];

    return function(){
        if ( arguments.length === 0 ){
            var money = 0;
            for ( var i = 0, l = args.length; i < l; i++ ){
                money += args[ i ];
            }
            return money;
        }else{
            [].push.apply( args, arguments );
        }
    }

})();

cost( 100 );    // 未真正求值
cost( 200 );    // 未真正求值
cost( 300 );    // 未真正求值

console.log( cost() );       // 求值并输出:600

接下来我们编写一个通用的function currying(){},function currying(){}接受一个参数,即将要被currying的函数。在这个例子里,这个函数的作用遍历本月每天的开销并求出它们的总和。代码如下:

var currying = function( fn ){
    var args = [];

    return function(){
        if ( arguments.length === 0 ){
            return fn.apply( this, args );
        }else{
            [].push.apply( args, arguments );
            return arguments.callee;
        }
    }

};

var cost = (function(){
    var money = 0;

    return function(){
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            money += arguments[ i ];
        }
        return money;
    }

})();

var cost = currying( cost );    // 转化成currying函数

cost( 100 );    // 未真正求值
cost( 200 );    // 未真正求值
cost( 300 );    // 未真正求值

alert ( cost() );     // 求值并输出:600

至此,我们完成了一个currying函数的编写。当调用cost()时,如果明确地带上了一些参数,表示此时并不进行真正的求值计算,而是把这些参数保存起来,此时让cost函数返回另外一个函数。只有当我们以不带参数的形式执行cost()时,才利用前面保存的所有参数,真正开始进行求值计算。

2. uncurrying

在JavaScript中,当我们调用对象的某个方法时,其实不用去关心该对象原本是否被设计为拥有这个方法,这是动态类型语言的特点,也是常说的鸭子类型思想。

同理,一个对象也未必只能使用它自身的方法,那么有什么办法可以让对象去借用一个原本不属于它的方法呢?

答案对于我们来说很简单,call和apply都可以完成这个需求:

var obj1 = {
    name: 'sven'
};

var obj2 = {
    getName: function(){
        return this.name;
    }
};

console.log( obj2.getName.call( obj1 ) );     // 输出:sven

我们常常让类数组对象去借用Array.prototype的方法,这是call和apply最常见的应用场景之一:

(function(){
    Array.prototype.push.call( arguments, 4 );    // arguments借用Array.prototype.push方法
    console.log( arguments );      // 输出:[1, 2, 3, 4]
})( 1, 2, 3 );

在我们的预期中,Array.prototype上的方法原本只能用来操作array对象。但用call和apply可以把任意对象当作this传入某个方法,这样一来,方法中用到this的地方就不再局限于原来规定的对象,而是加以泛化并得到更广的适用性。

那么有没有办法把泛化this的过程提取出来呢?下面讲述的uncurrying如何解决这个问题的。以下代码是uncurrying的实现方式之一:

Function.prototype.uncurrying = function () {
    var self = this;
    return function() {
        var obj = Array.prototype.shift.call( arguments );
        return self.apply( obj, arguments );
    };
};

在讲解这段代码的实现原理之前,我们先来瞧瞧它有什么作用。

在类数组对象arguments借用Array.prototype的方法之前,先把Array.prototype.push.call这句代码转换为一个通用的push函数:

var push = Array.prototype.push.uncurrying();

(function(){
    push( arguments, 4 );
    console.log( arguments );     // 输出:[1, 2, 3, 4]
})( 1, 2, 3 );

通过uncurrying的方式,Array.prototype.push.call变成了一个通用的push函数。这样一来,push函数的作用就跟Array.prototype.push一样了,同样不仅仅局限于只能操作array对象。而对于使用者而言,调用push函数的方式也显得更加简洁和意图明了。

我们还可以一次性地把Array.prototype上的方法“复制”到array对象上,同样这些方法可操作的对象也不仅仅只是array对象:

for ( var i = 0, fn, ary = [ 'push', 'shift', 'forEach' ]; fn = ary[ i++ ]; ){
    Array[ fn ] = Array.prototype[ fn ].uncurrying();
};

var obj = {
    "length": 3,
    "0": 1,
    "1": 2,
    "2": 3
};

Array.push( obj, 4 );     // 向对象中添加一个元素
console.log( obj.length );    // 输出:4

var first = Array.shift( obj );    // 截取第一个元素
console.log( first );     // 输出:1
console.log( obj );    // 输出:{0: 2, 1: 3, 2: 4, length: 3}

Array.forEach( obj, function( i, n ){
    console.log( n );      // 分别输出:0, 1, 2
});

甚至Function.prototype.call和Function.prototype.apply本身也可以被uncurrying,不过这没有实用价值,只是使得对函数的调用看起来更像JavaScript语言的前身Scheme:

var call = Function.prototype.call.uncurrying();
var fn = function( name ){
    console.log( name );
};
call( fn, window, 'sven' );     // 输出:sven

var apply = Function.prototype.apply.uncurrying();
var fn = function( name ){
    console.log( this.name );     // 输出:"sven"
    console.log( arguments );     // 输出: [1, 2, 3]
};
apply( fn, { name: 'sven' }, [ 1, 2, 3 ] );

目前我们已经给出了Function.prototype.uncurrying的一种实现。现在来分析调用Array.prototype.push.uncurrying()这句代码时发生了什么事情:

Function.prototype.uncurrying = function () {
    var self = this;     // self此时是Array.prototype.push
    return function() {
        var obj = Array.prototype.shift.call( arguments );
        // obj是{
        //    "length": 1,
        //    "0": 1
        // }
        // arguments对象的第一个元素被截去,剩下[2]
        return self.apply( obj, arguments );
        // 相当于Array.prototype.push.apply( obj, 2 )
    };
};

var push = Array.prototype.push.uncurrying();
var obj = {
    "length": 1,
    "0": 1
};

push( obj, 2 );
console.log( obj );     // 输出:{0: 1, 1: 2, length: 2}

除了刚刚提供的代码实现,下面的代码是uncurrying的另外一种实现方式:

Function.prototype.uncurrying = function(){
    var self = this;
    return function(){
        return Function.prototype.call.apply( self, arguments );
    }
};

3. 函数节流

JavaScript中的函数大多数情况下都是由用户主动调用触发的,除非是函数本身的实现不合理,否则我们一般不会遇到跟性能相关的问题。但在一些少数情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能被非常频繁地调用,而造成大的性能问题。下面将列举一些这样的场景。

(1) 函数被频繁调用的场景

window.onresize事件。我们给window对象绑定了resize事件,当浏览器窗口大小被拖动而改变的时候,这个事件触发的频率非常之高。如果我们在window.onresize事件函数里有一些跟DOM节点相关的操作,而跟DOM节点相关的操作往往是非常消耗性能的,这时候浏览器可能就会吃不消而造成卡顿现象。

mousemove事件。同样,如果我们给一个div节点绑定了拖曳事件(主要是mousemove),当div节点被拖动的时候,也会频繁地触发该拖曳事件函数。

上传进度。微云的上传功能使用了公司提供的一个浏览器插件。该浏览器插件在真正开始上传文件之前,会对文件进行扫描并随时通知JavaScript函数,以便在页面中显示当前的扫描进度。但该插件通知的频率非常之高,大约一秒钟10次,很显然我们在页面中不需要如此频繁地去提示用户。

(2) 函数节流的原理

我们整理上面提到的三个场景,发现它们面临的共同问题是函数被触发的频率太高。

比如我们在window.onresize事件中要打印当前的浏览器窗口大小,在我们通过拖曳来改变窗口大小的时候,打印窗口大小的工作1秒钟进行了10次。而我们实际上只需要2次或者3次。这就需要我们按时间段来忽略掉一些事件请求,比如确保在500ms内只打印一次。很显然,我们可以借助setTimeout来完成这件事情。

(3) 函数节流的代码实现

关于函数节流的代码实现有许多种,下面的throttle 函数的原理是,将即将被执行的函数用setTimeout延迟一段时间执行。如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求。throttle函数接受2个参数,第一个参数为需要被延迟执行的函数,第二个参数为延迟执行的时间。具体实现代码如下:

var throttle = function ( fn, interval ) {

    var __self = fn,    // 保存需要被延迟执行的函数引用
        timer,      // 定时器
        firstTime = true;    // 是否是第一次调用

    return function () {
        var args = arguments,
            __me = this;

        if ( firstTime ) {    // 如果是第一次调用,不需延迟执行
            __self.apply(__me, args);
            return firstTime = false;
        }

        if ( timer ) {    // 如果定时器还在,说明前一次延迟执行还没有完成
            return false;
        }

        timer = setTimeout(function () {  // 延迟一段时间执行
            clearTimeout(timer);
            timer = null;
            __self.apply(__me, args);

        }, interval || 500 );

    };

};

window.onresize = throttle(function(){
    console.log( 1 );
}, 500 );

4. 分时函数

在前面关于函数节流的讨论中,我们提供了一种限制函数被频繁调用的解决方案。下面我们将遇到另外一个问题,某些函数确实是用户主动调用的,但因为一些客观的原因,这些函数会严重地影响页面性能。

一个例子是创建WebQQ的QQ好友列表。列表中通常会有成百上千个好友,如果一个好友用一个节点来表示,当我们在页面中渲染这个列表的时候,可能要一次性往页面中创建成百上千个节点。

在短时间内往页面中大量添加DOM节点显然也会让浏览器吃不消,我们看到的结果往往就是浏览器的卡顿甚至假死。代码如下:

var ary = [];

for ( var i = 1; i <= 1000; i++ ){
    ary.push( i );     // 假设ary装载了1000个好友的数据
};

var renderFriendList = function( data ){
    for ( var i = 0, l = data.length; i < l; i++ ){
        var div = document.createElement( 'div' );
        div.innerHTML = i;
        document.body.appendChild( div );
    }
};

renderFriendList( ary );

这个问题的解决方案之一是下面的timeChunk函数,timeChunk 函数让创建节点的工作分批进行,比如把1秒钟创建1000个节点,改为每隔200毫秒创建8个节点。

timeChunk函数接受3个参数,第1个参数是创建节点时需要用到的数据,第2个参数是封装了创建节点逻辑的函数,第3个参数表示每一批创建的节点数量。代码如下:

var timeChunk = function( ary, fn, count ){

    var obj,
        t;

    var len = ary.length;

    var start = function(){
        for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ){
            var obj = ary.shift();
            fn( obj );
        }
     };

     return function(){
        t = setInterval(function(){
          if ( ary.length === 0 ){  // 如果全部节点都已经被创建好
              return clearInterval( t );
          }
          start();
        }, 200 );    // 分批执行的时间间隔,也可以用参数的形式传入

    };

};

最后我们进行一些小测试,假设我们有1000个好友的数据,我们利用timeChunk函数,每一批只往页面中创建8个节点:

var ary = [];

 for ( var i = 1; i <= 1000; i++ ){
     ary.push( i );
 };

 var renderFriendList = timeChunk( ary, function( n ){
     var div = document.createElement( 'div' );
     div.innerHTML = n;
     document.body.appendChild( div );
 }, 8 );

 renderFriendList();

5. 惰性加载函数

在Web开发中,因为浏览器之间的实现差异,一些嗅探工作总是不可避免。比如我们需要一个在各个浏览器中能够通用的事件绑定函数addEvent,常见的写法如下:

var addEvent = function( elem, type, handler ){
    if ( window.addEventListener ){
       return elem.addEventListener( type, handler, false );
      }
      if ( window.attachEvent ){
          return elem.attachEvent( 'on' + type, handler );
      }
};

这个函数的缺点是,当它每次被调用的时候都会执行里面的if条件分支,虽然执行这些if分支的开销不算大,但也许有一些方法可以让程序避免这些重复的执行过程。

第二种方案是这样,我们把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就立刻进行一次判断,以便让addEvent返回一个包裹了正确逻辑的函数。代码如下:

var addEvent = (function(){
    if ( window.addEventListener ){
        return function( elem, type, handler ){
            elem.addEventListener( type, handler, false );
        }
    }
    if ( window.attachEvent ){
        return function( elem, type, handler ){
            elem.attachEvent( 'on' + type, handler );
        }
    }
})();

目前的addEvent函数依然有个缺点,也许我们从头到尾都没有使用过addEvent函数,这样看来,前一次的浏览器嗅探就是完全多余的操作,而且这也会稍稍延长页面ready的时间。

第三种方案即是我们将要讨论的惰性载入函数方案。此时addEvent依然被声明为一个普通函数,在函数里依然有一些分支判断。但是在第一次进入条件分支之后,在函数内部会重写这个函数,重写之后的函数就是我们期望的addEvent函数,在下一次进入addEvent函数的时候,addEvent函数里不再存在条件分支语句:

<html>
    <body>
        <div id="div1">点我绑定事件</div>
    <script>

    var addEvent = function( elem, type, handler ){
        if ( window.addEventListener ){
           addEvent = function( elem, type, handler ){
               elem.addEventListener( type, handler, false );
           }
        }else if ( window.attachEvent ){
            addEvent = function( elem, type, handler ){
                elem.attachEvent( 'on' + type, handler );
            }
        }

        addEvent( elem, type, handler );
    };

      var div = document.getElementById( 'div1' );

      addEvent( div, 'click', function(){
          alert (1);
      });

      addEvent( div, 'click', function(){
          alert (2);
      });

    </script>
    </body>
</html>

小结

在JavaScript开发中,闭包和高阶函数的应用极多。就设计模式而言,因为JavaScript这门语言的自身特点,许多设计模式在JavaScript之中的实现跟在一些传统面向对象语言中的实现相差很大。在JavaScript中,很多设计模式都是通过闭包和高阶函数实现的。这并不奇怪,相对于模式的实现过程,我们更关注的是模式可以帮助我们完成什么。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,192评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,186评论 1 303
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,844评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,471评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,876评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,891评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,068评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,791评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,539评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,772评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,250评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,577评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,244评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,146评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,949评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,995评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,812评论 2 276

推荐阅读更多精彩内容