阮一峰es6要點總結——Generator

筆記,總結摘錄自阮一峰
筆記中有不少自己看書的總結

基本概念

核心目的:異步編程解決方案

關鍵概念:狀態(tài)機,執(zhí)行權限的傳遞,數(shù)據(jù)的傳入傳出

  • 寫法
  • function關鍵字和函數(shù)名之間有一個星號
  • 函數(shù)內部,使用yield語句,定義內部狀態(tài)
  • 作為對象屬性,可以簡寫
let obj = {
  * myGeneratorMethod() {
    ···
  }
};
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

調用Generator函數(shù),函數(shù)并不執(zhí)行,而是返回一個遍歷器對象。必須調用遍歷器對象的next方法,將函數(shù)執(zhí)行到下一個yield語句的地方。

當遇到yield語句,就暫停執(zhí)行,并將yield后邊的表達式的值,作為返回對象的value屬性值。

yield語句如果用在一個表達式之中,必須放在圓括號里面

yield語句用作函數(shù)參數(shù)或放在賦值表達式的右邊,可以不加括號

return語句,將返回表達式后邊的值,并結束函數(shù)。

  • 傳入與傳出

通過yield后的表達式,可以實現(xiàn)傳出

通過next(value)方法中,傳入?yún)?shù),可以實現(xiàn)傳入。value將作為上一個yield語句的返回值。

我們都知道,一個運算符,都有返回值,比如+號,比如=號,比如,號。正常情況下,yield語句的返回值,是undefined,當我們?yōu)?code>next方法傳入?yún)?shù)時候,在恢復執(zhí)行之前,會將yield的返回值替換為我們傳入的參數(shù)。

function* f() {
  for(var i = 0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }

所以,我們是可以對函數(shù)的行為進行控制的。這就非常方便了,比如,函數(shù)執(zhí)行權的交替。

碎知識

  • for...of配合
    比如,實現(xiàn)對象的遍歷
function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

也可以直接加到對象的Symbol.iterator

function* objectEntries() {
  let propKeys = Object.keys(this);

  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
  • Generator.prototype.throw()
    Generator函數(shù)返回的遍歷器對象,都有一個throw方法,可以在函數(shù)體外拋出錯誤,然后在Generator函數(shù)體內捕獲。
var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('內部捕獲', e);
  }
};

var i = g();
i.next();

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b

throw方法可以接受一個參數(shù),該參數(shù)會被catch語句接收,建議拋出Error對象的實例。

不管內外,必須有try...catch語句來捕獲錯誤,不然程序將報錯。

throw方法被捕獲以后,會附帶執(zhí)行到下一條yield語句。(他就是它本身帶了一個next方法)

一旦Generator執(zhí)行過程中拋出錯誤,且沒有被內部捕獲,就不會再執(zhí)行下去了。如果此后還調用next方法,將返回一個value屬性等于undefineddone屬性等于true的對象,即JavaScript引擎認為這個Generator已經運行結束了。

function* g() {
  yield 1;
  console.log('throwing an exception');
  throw new Error('generator broke!');
  yield 2;
  yield 3;
}

function log(generator) {
  var v;
  console.log('starting generator');
  try {
    v = generator.next();
    console.log('第一次運行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  try {
    v = generator.next();
    console.log('第二次運行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  try {
    v = generator.next();
    console.log('第三次運行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  console.log('caller done');
}

log(g());
// starting generator
// 第一次運行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉錯誤 { value: 1, done: false }
// 第三次運行next方法 { value: undefined, done: true }
// caller done
  • Generator.prototype.return()

Generator函數(shù)返回的遍歷器對象,還有一個return方法,可以返回給定的值,并且終結遍歷Generator函數(shù)。

如果return(value)方法傳值了,那就返回傳入的那個值,如果沒有傳值,就返回undefined

如果Generator函數(shù)內部有try...finally代碼塊,那么當遇到return時候,不會立刻結束,而是會把finally代碼塊中的執(zhí)行完,然后再return

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
  • yield*
    在Generator函數(shù)調用另一個Generator函數(shù)時候,要用yield*

等價于,在yield*位置,展開另一個Generator函數(shù)的狀態(tài)。

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"

任何數(shù)據(jù)結構只要有Iterator接口,就可以被yield*遍歷

如果被代理的Generator函數(shù)有return語句,那么就可以向代理它的Generator函數(shù)返回數(shù)據(jù)。

function *foo() {
  yield 2;
  yield 3;
  return "foo";
}

function *bar() {
  yield 1;
  var v = yield *foo();
  console.log( "v: " + v );
  yield 4;
}

var it = bar();

it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}

yield*命令可以很方便地取出嵌套數(shù)組的所有成員。

// 下面是二叉樹的構造函數(shù),
// 三個參數(shù)分別是左樹、當前節(jié)點和右樹
function Tree(left, label, right) {
  this.left = left;
  this.label = label;
  this.right = right;
}

// 下面是中序(inorder)遍歷函數(shù)。
// 由于返回的是一個遍歷器,所以要用generator函數(shù)。
// 函數(shù)體內采用遞歸算法,所以左樹和右樹要用yield*遍歷
function* inorder(t) {
  if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
  }
}

// 下面生成二叉樹
function make(array) {
  // 判斷是否為葉節(jié)點
  if (array.length == 1) return new Tree(null, array[0], null);
  return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);

// 遍歷二叉樹
var result = [];
for (let node of inorder(tree)) {
  result.push(node);
}

result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
  • Generator函數(shù)中的this

Generator函數(shù),不能跟new操作符一起使用,會報錯

把Generator函數(shù),當成構造函數(shù),會失效,因為它總是返回遍歷器對象,而不是那個新創(chuàng)建的對象。

一個方法解決

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {};
var f = F.call(obj);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3

Generator 與 協(xié)程

協(xié)程(coroutine)是一種程序運行的方式,可以理解成“協(xié)作的線程”或“協(xié)作的函數(shù)”。

協(xié)程既可以用單線程實現(xiàn),是一種特殊的子例程。
也可以用多線程實現(xiàn),是一種特殊的線程。

  • 協(xié)程與子例程的差異

傳統(tǒng)的“子例程”(subroutine)采用堆棧式“后進先出”的執(zhí)行方式,只有當調用的子函數(shù)完全執(zhí)行完畢,才會結束執(zhí)行父函數(shù)。協(xié)程與其不同,多個線程(單線程情況下,即多個函數(shù))可以并行執(zhí)行,但是只有一個線程(或函數(shù))處于正在運行的狀態(tài),其他線程(或函數(shù))都處于暫停態(tài)(suspended),線程(或函數(shù))之間可以交換執(zhí)行權。也就是說,一個線程(或函數(shù))執(zhí)行到一半,可以暫停執(zhí)行,將執(zhí)行權交給另一個線程(或函數(shù)),等到稍后收回執(zhí)行權的時候,再恢復執(zhí)行。這種可以并行執(zhí)行、交換執(zhí)行權的線程(或函數(shù)),就稱為協(xié)程。

從實現(xiàn)上看,在內存中,子例程只使用一個棧(stack),而協(xié)程是同時存在多個棧,但只有一個棧是在運行狀態(tài),也就是說,協(xié)程是以多占用內存為代價,實現(xiàn)多任務的并行。

  • 協(xié)程與普通線程的差異

不難看出,協(xié)程適合用于多任務運行的環(huán)境。在這個意義上,它與普通的線程很相似,都有自己的執(zhí)行上下文、可以分享全局變量。它們的不同之處在于,同一時間可以有多個線程處于運行狀態(tài),但是運行的協(xié)程只能有一個,其他協(xié)程都處于暫停狀態(tài)。此外,普通的線程是搶先式的,到底哪個線程優(yōu)先得到資源,必須由運行環(huán)境決定,但是協(xié)程是合作式的,執(zhí)行權由協(xié)程自己分配。

由于ECMAScript是單線程語言,只能保持一個調用棧。引入?yún)f(xié)程以后,每個任務可以保持自己的調用棧。這樣做的最大好處,就是拋出錯誤的時候,可以找到原始的調用棧。不至于像異步操作的回調函數(shù)那樣,一旦出錯,原始的調用棧早就結束。

Generator函數(shù)是ECMAScript 6對協(xié)程的實現(xiàn),但屬于不完全實現(xiàn)。Generator函數(shù)被稱為“半?yún)f(xié)程”(semi-coroutine),意思是只有Generator函數(shù)的調用者,才能將程序的執(zhí)行權還給Generator函數(shù)。如果是完全執(zhí)行的協(xié)程,任何函數(shù)都可以讓暫停的協(xié)程繼續(xù)執(zhí)行。

如果將Generator函數(shù)當作協(xié)程,完全可以將多個需要互相協(xié)作的任務寫成Generator函數(shù),它們之間使用yield語句交換控制權。

應用

改寫異步操作——同步化表達

例子一:Ajax操作

function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url) {
  makeAjaxCall(url, function(response){
    it.next(response);
  });
}

var it = main();
it.next();

注意request方法里邊,請求到后,調用next方法,要傳參數(shù)進去。

例子二:逐行讀取文本

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while(!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}

控制流管理

例子一:同步方法的流管理

function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}
scheduler(longRunningTask(initialValue));

function scheduler(task) {
  var taskObj = task.next(task.value);
  // 如果Generator函數(shù)未結束,就繼續(xù)調用
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task);
  }
}

例子二:利用for...of

let steps = [step1Func, step2Func, step3Func];

function *iterateSteps(steps){
  for (var i=0; i< steps.length; i++){
    var step = steps[i];
    yield step();
  }
}

以上將任務,分成多個步驟

let jobs = [job1, job2, job3];

function *iterateJobs(jobs){
  for (var i=0; i< jobs.length; i++){
    var job = jobs[i];
    yield *iterateSteps(job.steps);
  }
}

又將項目,分成多個任務。

最后,用for...of循環(huán),一次性執(zhí)行所有任務的所有步驟。

for (var step of iterateJobs(jobs)){
  console.log(step.id);
}

部署Iterator接口

function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// foo 3
// bar 7

異步應用

異步編程的方法

  • 回調函數(shù)
  • 事件監(jiān)聽
  • 發(fā)布/訂閱模式
  • Promise 對象

ES6,有了Generator

幾種方法的分析

回調函數(shù),在寫法上,會出現(xiàn)多重嵌套的問題(回調地獄)

Promise解決了這個問題,但是引入了大量的Promise語法。總體來說,沒有新意。

Generator函數(shù)是協(xié)程的ES6實現(xiàn),通過執(zhí)行權的交替,實現(xiàn)了異步編程。

Generator的自動流程管理

  • 回到函數(shù),封裝Thunk函數(shù)
  • Promise

Thunk 函數(shù)

參數(shù)的求職策略

即,函數(shù)的參數(shù)到底何時求值?

  • 傳值調用
    在進入函數(shù)體之前。c用的傳值調用
  • 傳名調用
    在真正用到時候,再求值。

Thunk函數(shù),在傳名調用實現(xiàn)中,將參數(shù)放入一個臨時函數(shù)之中,再將這個臨時函數(shù),傳入函數(shù)體。這個臨時函數(shù),就是Thunk函數(shù)。

function f(m) {
  return m * 2;
}

f(x + 5);

// 等同于

var thunk = function () {
  return x + 5;
};

function f(thunk) {
  return thunk() * 2;
}

JavaScript的Thunk函數(shù)

js是傳值調用,它的Thunk函數(shù),替換的是多參數(shù)函數(shù),將其替換成,只接受回調函數(shù)作為參數(shù)的單參數(shù)函數(shù)。

// 正常版本的readFile(多參數(shù)版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(單參數(shù)版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

也就是說,我們通過thunk將回到函數(shù),普通參數(shù)分離開來傳入。這為我們交替執(zhí)行權做準備。

下邊是簡單的Thunk函數(shù)轉換器

// ES5版本
var Thunk = function(fn){
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      return fn.apply(this, args);
    }
  };
};

// ES6版本
var Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};

使用方法

var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);

生產環(huán)境的Thunk

建議使用Thunkify模塊
它的源碼,與上邊的很像

function thunkify(fn) {
  return function() {
    var args = new Array(arguments.length);
    var ctx = this;

    for (var i = 0; i < args.length; ++i) {
      args[i] = arguments[i];
    }

    return function (done) {
      var called;

      args.push(function () {
        if (called) return;
        called = true;
        done.apply(null, arguments);
      });

      try {
        fn.apply(ctx, args);
      } catch (err) {
        done(err);
      }
    }
  }
};

它的源碼主要多了一個檢查機制,變量called確保回調函數(shù)只運行一次。

重點:利用Thunk的 Generator函數(shù)自動流程管理

由于多個異步操作,需要保證前一步執(zhí)行完,再執(zhí)行后一步。可以這樣

var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);

var gen = function* (){
  var r1 = yield readFileThunk('/etc/fstab');
  console.log(r1.toString());
  var r2 = yield readFileThunk('/etc/shells');
  console.log(r2.toString());
};

兩個異步操作,在執(zhí)行他們時候,把執(zhí)行權交給了另一個函數(shù),然后,在另一個函數(shù)(readFileThunk)中,當異步操作完成,我們再把執(zhí)行權返還給gen,這樣就保證了前一步完成,再執(zhí)行下一步。

現(xiàn)在我們有Thunk函數(shù),有了Generator函數(shù),關鍵就是,自動執(zhí)行的函數(shù)怎么寫。

我們的目的是,在Thunk的回調函數(shù)與Generator之間進行切換

function run(fn) {
  var gen = fn();

  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
    // 注意這一步,result.value是gen函數(shù)中的readFileThunk,
    // 它是一個Thunk函數(shù),也就是說,這一步等價于readFileThunk(file1)(next)
  }

  next();
}

function* g() {
  // ...
}

run(g);

上邊的函數(shù)中:
第一步——next就是Thunk函數(shù)的回調函數(shù),首先將執(zhí)行權還給gen函數(shù)
第二步——gen 函數(shù)執(zhí)行到y(tǒng)ield語句,將執(zhí)行權交給next函數(shù),并把需要的異步操作一并傳給next函數(shù)。
第三步——run函數(shù)收到返回的對象,判斷流程是否結束,如果沒有結束,直接調用傳回的異步操作,即result.value(next),等價于readFileThunk(file1)(next)
第四步——異步操作完成,將會調用回到函數(shù)next,并將請求的數(shù)據(jù)傳入next函數(shù)。此時,next函數(shù)重復第一步,并將數(shù)據(jù)傳回gen函數(shù)。

如此往復循環(huán),知道結束

也就是說,異步操作的回調函數(shù),用來控制流程,并且將數(shù)據(jù)原封不動的傳回給gen函數(shù)。真正對請求數(shù)據(jù)的操作不是在回調函數(shù)里做的,而是在gen函數(shù)里。

co模塊——基于Promise對象的Generator函數(shù)自動流程管理

TJ Holowaychuk發(fā)布的小工具,就是上邊的run函數(shù),只不過用的是Promise

一個例子:

var fs = require('fs');

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) return reject(error);
      resolve(data);
    });
  });
};

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

首先將異步操作封裝成Promise(上一部分是封裝成Thunk)。

如果手動執(zhí)行:

var g = gen();

g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
});

其實就是一堆then,,,

ok,接下來寫自動執(zhí)行器

function run(gen){
  var g = gen();

  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }

  next();
}

run(gen);

跟上一部分沒什么差別。用Promise編程而已

來看看co的源碼

最外層,co接受一個Generator,返回一個Promise

function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
  });
}

在返回的Promise對象中,首先檢查gen是否為Generator,如果是,就執(zhí)行,如果不是,就返回,并將Promise的狀態(tài)resolved

function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
  });
}

co將next方法抽象為兩部分,1-執(zhí)行權交給gen,2-調用自身。在第一部分加入了出錯處理,使得方法更健壯。

function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();
    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);// 關鍵部分
    }
  });
}

最關鍵的是新的next函數(shù)

function next(ret) {
  if (ret.done) return resolve(ret.value);
  var value = toPromise.call(ctx, ret.value);
  if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
  return onRejected(
    new TypeError(
      'You may only yield a function, promise, generator, array, or object, '
      + 'but the following object was passed: "'
      + String(ret.value)
      + '"'
    )
  );
}

如果gen結束了,那執(zhí)行器也結束
第二行,保證gen傳過來的每一步,都是Promise
第三行,如果確實是Promise,就執(zhí)行異步操作,并設置onFulfilled為成功的回調函數(shù)。
第四行,如果參數(shù)不符合要求,終止執(zhí)行,并將返回的Promise狀態(tài)改為rejected。

co支持并發(fā)的異步操作
并發(fā),即,某些操作同時進行,等到他們全部完成,才執(zhí)行下一步

將并發(fā)的操作放在數(shù)組/對象里邊,跟在yield語句后邊即可

// 數(shù)組的寫法
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res);
}).catch(onerror);

// 對象的寫法
co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  };
  console.log(res);
}).catch(onerror);

另一個例子

co(function* () {
  var values = [n1, n2, n3];
  yield values.map(somethingAsync);
});

function* somethingAsync(x) {
  // do something async
  return y
}

處理Stream

Stream,就是將整個數(shù)據(jù),一塊塊挨著處理。

Stream模式使用EventEmitter API,會釋放三個事件

  • data事件:下一塊數(shù)據(jù)準備好了
  • end事件:整個數(shù)據(jù)流處理完了
  • error事件:發(fā)生錯誤

利用Promise.race()函數(shù),可以判斷只有當data事件發(fā)生,才進入下一個數(shù)據(jù)塊的處理。

const co = require('co');
const fs = require('fs');

const stream = fs.createReadStream('./les_miserables.txt');
let valjeanCount = 0;

co(function*() {
  while(true) {
    const res = yield Promise.race([
      new Promise(resolve => stream.once('data', resolve)),
      new Promise(resolve => stream.once('end', resolve)),
      new Promise((resolve, reject) => stream.once('error', reject))
    ]);
    if (!res) {
      break;
    }
    stream.removeAllListeners('data');
    stream.removeAllListeners('end');
    stream.removeAllListeners('error');
    valjeanCount += (res.toString().match(/valjean/ig) || []).length;
  }
  console.log('count:', valjeanCount); // count: 1120
});
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容

  • 異步編程對JavaScript語言太重要。Javascript語言的執(zhí)行環(huán)境是“單線程”的,如果沒有異步編程,根本...
    呼呼哥閱讀 7,321評論 5 22
  • 簡介 基本概念 Generator函數(shù)是ES6提供的一種異步編程解決方案,語法行為與傳統(tǒng)函數(shù)完全不同。本章詳細介紹...
    呼呼哥閱讀 1,082評論 0 4
  • 在此處先列下本篇文章的主要內容 簡介 next方法的參數(shù) for...of循環(huán) Generator.prototy...
    醉生夢死閱讀 1,457評論 3 8
  • 本文為阮一峰大神的《ECMAScript 6 入門》的個人版提純! babel babel負責將JS高級語法轉義,...
    Devildi已被占用閱讀 1,999評論 0 4
  • 特別說明,為便于查閱,文章轉自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 490評論 0 0