ES6(十八):迭代器(Iterator)和生成器(Generator)

前面的話


??用循環(huán)語句迭代數(shù)據(jù)時完箩,必須要初始化一個變量來記錄每一次迭代在數(shù)據(jù)集合中的位置润文,而在許多編程語言中仿贬,已經(jīng)開始通過程序化的方式用迭代器對象返回迭代過程中集合的每一個元素

迭代器的使用可以極大地簡化數(shù)據(jù)操作啥箭,于是ES6也向JS中添加了這個迭代器特性扬蕊。新的數(shù)組方法和新的集合類型(如Set集合與Map集合)都依賴迭代器的實現(xiàn),這個新特性對于高效的數(shù)據(jù)處理而言是不可或缺的怕享,在語言的其他特性中也都有迭代器的身影:新的for-of循環(huán)、展開運算符(...)镰踏,甚至連異步編程都可以使用迭代器

本文將詳細介紹ES6中的迭代器(Iterator)和生成器(Generator)

引入

下面是一段標(biāo)準(zhǔn)的for循環(huán)代碼函筋,通過變量i來跟蹤colors數(shù)組的索引,循環(huán)每次執(zhí)行時奠伪,如果i小于數(shù)組長度len則加1跌帐,并執(zhí)行下一次循環(huán)

var colors = ["red", "green", "blue"];
for (var i = 0, len = colors.length; i < len; i++) {
  console.log(colors[i]);
}
  • 雖然循環(huán)語句語法簡單,但如果將多個循環(huán)嵌套則需要追蹤多個變量绊率,代碼復(fù)雜度會大大增加谨敛,一不小心就錯誤使用了其他for循環(huán)的跟蹤變量,從而導(dǎo)致程序出錯滤否。迭代器的出現(xiàn)旨在消除這種復(fù)雜性并減少循環(huán)中的錯誤

迭代器

迭代器是一種特殊對象脸狸,它具有一些專門為迭代過程設(shè)計的專有接口,所有的迭代器對象都有一個next()方法,每次調(diào)用都返回一個結(jié)果對象炊甲。結(jié)果對象有兩個屬性:一個是value泥彤,表示下一個將要返回的值;另一個是done卿啡,它是一個布爾類型的值吟吝,當(dāng)沒有更多可返回數(shù)據(jù)時返回true。迭代器還會保存一個內(nèi)部指針颈娜,用來指向當(dāng)前集合中值的位置剑逃,每調(diào)用一次next()方法,都會返回下一個可用的值

如果在最后一個值返回后再調(diào)用next()方法官辽,那么返回的對象中屬性done的值為true蛹磺,屬性value則包含迭代器最終返回的值,這個返回值不是數(shù)據(jù)集的一部分野崇,它與函數(shù)的返回值類似称开,是函數(shù)調(diào)用過程中最后一次給調(diào)用者傳遞信息的方法,如果沒有相關(guān)數(shù)據(jù)則返回undefined

下面用ES5的語法創(chuàng)建一個迭代器

function createIterator(items) {
  var i = 0;    return {
    next: function() {            
      var done = (i >= items.length);
      var value = !done ? items[i++] : undefined;            
      return {
        done: done,
        value: value
      };
    }
  };
}
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"http:// 之后的所有調(diào)用
console.log(iterator.next()); // "{ value: undefined, done: true }"
  • 在上面這段代碼中乓梨,createIterator()方法返回的對象有一個next()方法鳖轰,每次調(diào)用時,items數(shù)組的下一個值會作為value返回扶镀。當(dāng)i為3時蕴侣,done變?yōu)?code>true;此時三元表達式會將value的值設(shè)置為undefined臭觉。最后兩次調(diào)用的結(jié)果與ES6迭代器的最終返回機制類似昆雀,當(dāng)數(shù)據(jù)集被用盡后會返回最終的內(nèi)容

  • 上面這個示例很復(fù)雜,而在ES6中蝠筑,迭代器的編寫規(guī)則也同樣復(fù)雜狞膘,但ES6同時還引入了一個生成器對象,它可以讓創(chuàng)建迭代器對象的過程變得更簡單

生成器

生成器是一種返回迭代器的函數(shù)什乙,通過function關(guān)鍵字后的星號(*)來表示挽封,函數(shù)中會用到新的關(guān)鍵字yield。星號可以緊挨著function關(guān)鍵字臣镣,也可以在中間添加一個空格

// 生成器
function *createIterator() {
  yield 1;
  yield 2;
  yield 3;
}
// 生成器能像正規(guī)函數(shù)那樣被調(diào)用辅愿,但會返回一個迭代器
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
  • 在這個示例中,createlterator()前的星號表明它是一個生成器忆某;yield關(guān)鍵字也是ES6的新特性点待,可以通過它來指定調(diào)用迭代器的next()方法時的返回值及返回順序。生成迭代器后弃舒,連續(xù)3次調(diào)用它的next()方法返回3個不同的值癞埠,分別是1、2和3。生成器的調(diào)用過程與其他函數(shù)一樣燕差,最終返回的是創(chuàng)建好的迭代器

  • 生成器函數(shù)最有趣的部分是遭笋,每當(dāng)執(zhí)行完一條yield語句后函數(shù)就會自動停止執(zhí)行。舉個例子徒探,在上面這段代碼中瓦呼,執(zhí)行完語句yield 1之后,函數(shù)便不再執(zhí)行其他任何語句测暗,直到再次調(diào)用迭代器的next()方法才會繼續(xù)執(zhí)行yield2語句央串。生成器函數(shù)的這種中止函數(shù)執(zhí)行的能力有很多有趣的應(yīng)用

  • 使用yield關(guān)鍵字可以返回任何值或表達式,所以可以通過生成器函數(shù)批量地給迭代器添加元素碗啄。例如质和,可以在循環(huán)中使用yield關(guān)鍵字

function *createIterator(items) {    
for (let i = 0; i < items.length; i++) {
  yield items[i];
  }
}
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"http:// 之后的所有調(diào)用
console.log(iterator.next()); // "{ value: undefined, done: true }"
  • 在此示例中,給生成器函數(shù)createlterator()傳入一個items數(shù)組稚字,而在函數(shù)內(nèi)部饲宿,for循環(huán)不斷從數(shù)組中生成新的元素放入迭代器中,每遇到一個yield語句循環(huán)都會停止胆描;每次調(diào)用迭代器的next()方法瘫想,循環(huán)會繼續(xù)運行并執(zhí)行下一條yield語句

  • 生成器函數(shù)是ES6中的一個重要特性,可以將其用于所有支持函數(shù)使用的地方

【使用限制】

yield關(guān)鍵字只可在生成器內(nèi)部使用昌讲,在其他地方使用會導(dǎo)致程序拋出錯誤

function *createIterator(items) {
  items.forEach(function(item) {       
    // 語法錯誤
    yield item + 1;
  });
}
  • 從字面上看国夜,yield關(guān)鍵字確實在createlterator()函數(shù)內(nèi)部,但是它與return關(guān)鍵字一樣短绸,二者都不能穿透函數(shù)邊界车吹。嵌套函數(shù)中的return語句不能用作外部函數(shù)的返回語句,而此處嵌套函數(shù)中的yield語句會導(dǎo)致程序拋出語法錯誤

【生成器函數(shù)表達式】

也可以通過函數(shù)表達式來創(chuàng)建生成器醋闭,只需在function關(guān)鍵字和小括號中間添加一個星號(*)即可

let createIterator = function *(items) {    
for (let i = 0; i < items.length; i++) {
  yield items[i];
  }
};
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"http:// 之后的所有調(diào)用
console.log(iterator.next()); // "{ value: undefined, done: true }"
  • 在這段代碼中窄驹,createlterator()是一個生成器函數(shù)表達式,而不是一個函數(shù)聲明证逻。由于函數(shù)表達式是匿名的馒吴,因此星號直接放在function關(guān)鍵字和小括號之間。此外瑟曲,這個示例基本與前例相同,使用的也是for循環(huán)

[注意]不能用箭頭函數(shù)來創(chuàng)建生成器

【生成器對象的方法】

由于生成器本身就是函數(shù)豪治,因而可以將它們添加到對象中洞拨。例如,在ES5風(fēng)格的對象字面量中负拟,可以通過函數(shù)表達式來創(chuàng)建生成器

var o = {
  createIterator: function *(items) {            
    for (let i = 0; i < items.length; i++) {
      yield items[i];
    }
  }
};
let iterator = o.createIterator([1, 2, 3]);
  • 也可以用ES6的函數(shù)方法的簡寫方式來創(chuàng)建生成器烦衣,只需在函數(shù)名前添加一個星號(*)
var o = {
  *createIterator(items) {            
    for (let i = 0; i < items.length; i++) {
      yield items[i];
    }
  }
};
let iterator = o.createIterator([1, 2, 3]);
  • 這些示例使用了不同于之前的語法,但它們的功能實際上是等價的。在簡寫版本中花吟,由于不使用function關(guān)鍵字來定義createlterator()方法秸歧,因此盡管可以在星號和方法名之間留白,但還是將星號緊貼在方法名之前

【狀態(tài)機】

生成器的一個常用功能是生成狀態(tài)機

let state = function*(){    
  while(1){
    yield 'A';
    yield 'B';
    yield 'C';
  }
}
let status = state();
console.log(status.next().value);//'A'
console.log(status.next().value);//'B'
console.log(status.next().value);//'C'
console.log(status.next().value);//'A'
console.log(status.next().value);//'B'

可迭代對象

可迭代對象具有Symbol.iterator屬性衅澈,是一種與迭代器密切相關(guān)的對象键菱。Symbol.iterator通過指定的函數(shù)可以返回一個作用于附屬對象的迭代器。在ES6中今布,所有的集合對象(數(shù)組经备、Set集合及Map集合)和字符串都是可迭代對象,這些對象中都有默認(rèn)的迭代器部默。ES6中新加入的特性for-of循環(huán)需要用到可迭代對象的這些功能

[注意]由于生成器默認(rèn)會為Symbol.iterator屬性賦值侵蒙,因此所有通過生成器創(chuàng)建的迭代器都是可迭代對象

  • 一開始,我們曾提到過循環(huán)內(nèi)部索引跟蹤的相關(guān)問題傅蹂,要解決這個問題纷闺,需要兩個工具:一個是迭代器,另一個是for-of循環(huán)份蝴。如此一來犁功,便不需要再跟蹤整個集合的索引,只需關(guān)注集合中要處理的內(nèi)容

  • for-of循環(huán)每執(zhí)行一次都會調(diào)用可迭代對象的next()方法搞乏,并將迭代器返回的結(jié)果對象的value屬性存儲在一個變量中波桩,循環(huán)將持續(xù)執(zhí)行這一過程直到返回對象的done屬性的值為true。這里有個示例

let values = [1, 2, 3];
for (let num of values) {
  //1
  //2
  //3 
  console.log(num);
}
  • 這段for-of循環(huán)的代碼通過調(diào)用values數(shù)組的Symbol.iterator方法來獲取迭代器请敦,這一過程是在JS引擎背后完成的镐躲。隨后迭代器的next()方法被多次調(diào)用,從其返回對象的value屬性讀取值并存儲在變量num中侍筛,依次為1萤皂、2和3,當(dāng)結(jié)果對象的done屬性值為true時循環(huán)退出匣椰,所以num不會被賦值為undefined`

  • 如果只需迭代數(shù)組或集合中的值裆熙,用for-of循環(huán)代替for循環(huán)是個不錯的選擇。相比傳統(tǒng)的for循環(huán)禽笑,for-of循環(huán)的控制條件更簡單入录,不需要追蹤復(fù)雜的條件,所以更少出錯

[注意]如果將for-of語句用于不可迭代對象佳镜、nullundefined將會導(dǎo)致程序拋出錯誤

【訪問默認(rèn)迭代器】

可以通過Symbol.iterator來訪問對象默認(rèn)的迭代器

let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }" //
  • 在這段代碼中僚稿,通過Symbol.iterator獲取了數(shù)組values的默認(rèn)迭代器,并用它遍歷數(shù)組中的元素蟀伸。在JS引擎中執(zhí)行for-of循環(huán)語句時也會有類似的處理過程

  • 由于具有Symbol.iterator屬性的對象都有默認(rèn)的迭代器蚀同,因此可以用它來檢測對象是否為可迭代對象

function isIterable(object) {
  return typeof object[Symbol.iterator] === "function";
}
console.log(isIterable([1, 2, 3])); // true
console.log(isIterable("Hello")); // true
console.log(isIterable(new Map())); // true
console.log(isIterable(new Set())); // true
console.log(isIterable(new WeakMap())); // false
console.log(isIterable(new WeakSet())); // false
  • 這里的islterable()函數(shù)可以檢查指定對象中是否存在默認(rèn)的函數(shù)類型迭代器缅刽,而for-of循環(huán)在執(zhí)行前也會做相似的檢查

  • 除了使用內(nèi)建的可迭代對象類型的Symbol.iterator,也可以使用Symbol.iterator來創(chuàng)建屬于自己的迭代器

【創(chuàng)建可迭代對象】

默認(rèn)情況下蠢络,開發(fā)者定義的對象都是不可迭代對象衰猛,但如果給Symbol.iterator屬性添加一個生成器,則可以將其變?yōu)榭傻鷮ο?/p>

let collection = {
  items: [],*[Symbol.iterator]() {        
    for (let item of this.items) {
      yield item;
    }
  }
};
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for (let x of collection) {
  //1
  //2
  //3 
  console.log(x);
}
  • 在這個示例中刹孔,先創(chuàng)建一個生成器(注意啡省,星號仍然在屬性名前)并將其賦值給對象的Symbol.iterator屬性來創(chuàng)建默認(rèn)的迭代器;而在生成器中芦疏,通過for-of循環(huán)迭代this.items并用yield返回每一個值冕杠。collection對象默認(rèn)迭代器的返回值由迭代器this.items自動生成,而非手動遍歷來定義返回值

【展開運算符和非數(shù)組可迭代對象】

通過展開運算符(...)可以把Set集合轉(zhuǎn)換成一個數(shù)組

let set = new Set([1, 2, 3, 3, 3, 4, 5]),
array = [...set];
console.log(array); // [1,2,3,4,5]
  • 這段代碼中的展開運算符把Set集合的所有值填充到了一個數(shù)組字面量里酸茴,它可以操作所有可迭代對象分预,并根據(jù)默認(rèn)迭代器來選取要引用的值,從迭代器讀取所有值薪捍。然后按照返回順序?qū)⑺鼈円来尾迦氲綌?shù)組中笼痹。Set集合是一個可迭代對象,展開運算符也可以用于其他可迭代對象
let map = new Map([ ["name", "huochai"], ["age", 25]]),
array = [...map];
console.log(array); // [ ["name", "huochai"], ["age", 25]]
  • 展開運算符把Map集合轉(zhuǎn)換成包含多個數(shù)組的數(shù)組酪穿,Map集合的默認(rèn)迭代器返回的是多組鍵值對凳干,所以結(jié)果數(shù)組與執(zhí)行new Map()時傳入的數(shù)組看起來一樣

  • 在數(shù)組字面量中可以多次使用展開運算符,將可迭代對象中的多個元素依次插入新數(shù)組中被济,替換原先展開運算符所在的位置

let smallNumbers = [1, 2, 3],
bigNumbers = [100, 101, 102],
allNumbers = [0, ...smallNumbers, ...bigNumbers];
console.log(allNumbers.length); // 7
console.log(allNumbers); // [0, 1, 2, 3, 100, 101, 102]
  • 創(chuàng)建一個變量allNumbers救赐,用展開運算符將smallNumbersbigNumbers里的值依次添加到allNumbers中。首先存入0只磷,然后存入small中的值经磅,最后存入bigNumbers中的值。當(dāng)然钮追,原始數(shù)組中的值只是被復(fù)制到allNumbers中预厌,它們本身并未改變

  • 由于展開運算符可以作用于任意可迭代對象,因此如果想將可迭代對象轉(zhuǎn)換為數(shù)組元媚,這是最簡單的方法轧叽。既可以將字符串中的每一個字符(不是編碼單元)存入新數(shù)組中,也可以將瀏覽器中NodeList對象中的每一個節(jié)點存入新的數(shù)組中

內(nèi)建迭代器

迭代器是ES6的一個重要組成部分刊棕,在ES6中炭晒,已經(jīng)默認(rèn)為許多內(nèi)建類型提供了內(nèi)建迭代器,只有當(dāng)這些內(nèi)建迭代器無法實現(xiàn)目標(biāo)時才需要自己創(chuàng)建甥角。通常來說當(dāng)定義自己的對象和類時才會遇到這種情況腰埂,否則,完全可以依靠內(nèi)建的迭代器完成工作蜈膨,而最常使用的可能是集合的那些迭代器

【集合對象迭代器】

ES6中有3種類型的集合對象:數(shù)組屿笼、Map集合與Set集合

為了更好地訪問對象中的內(nèi)容,這3種對象都內(nèi)建了以下三種迭代器

entries() 返回一個迭代器翁巍,其值為多個鍵值對
values() 返回一個迭代器驴一,其值為集合的值
keys() 返回一個迭代器,其值為集合中的所有鍵名
  • 調(diào)用以上3個方法都可以訪問集合的迭代器

entries()迭代器

每次調(diào)用next()方法時灶壶,entries()迭代器都會返回一個數(shù)組肝断,數(shù)組中的兩個元素分別表示集合中每個元素的鍵與值。如果被遍歷的對象是數(shù)組驰凛,則第一個元素是數(shù)字類型的索引胸懈;如果是Set集合,則第一個元素與第二個元素都是值(Set集合中的值被同時作為鍵與值使用)恰响;如果是Map集合趣钱,則第一個元素為鍵名

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for (let entry of colors.entries()) {
  console.log(entry);
}
for (let entry of tracking.entries()) {
  console.log(entry);
}
for (let entry of data.entries()) {
  console.log(entry);
}
  • 調(diào)用console.log()方法后輸出以下內(nèi)容
[0, "red"]
[1, "green"]
[2, "blue"]
[1234, 1234]
[5678, 5678]
[9012, 9012]
["title", "Understanding ES6"]
["format", "ebook"]
  • 在這段代碼中,調(diào)用每個集合的entries()方法獲取一個迭代器胚宦,并使用for-of循環(huán)來遍歷元素首有,且通過console將每一個對象的鍵值對輸出出來

values()迭代器

調(diào)用values()迭代器時會返回集合中所存的所有值

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for (let value of colors.values()) {
  console.log(value);
}
for (let value of tracking.values()) {
  console.log(value);
}
for (let value of data.values()) {
  console.log(value);
}
  • 調(diào)用console.log()方法后輸出以下內(nèi)容
"red"
"green"
"blue"
1234
5678
9012
"Understanding ES6"
"ebook"
  • 如上所示,調(diào)用values()迭代器后枢劝,返回的是每個集合中包含的真正數(shù)據(jù)井联,而不包含數(shù)據(jù)在集合中的位置信息

keys()迭代器

  • keys()迭代器會返回集合中存在的每一個鍵。如果遍歷的是數(shù)組您旁,則會返回數(shù)字類型的鍵烙常,數(shù)組本身的其他屬性不會被返回;如果是Set集合鹤盒,由于鍵與值是相同的蚕脏,因此keys()values()返回的也是相同的迭代器;如果是Map集合昨悼,則keys()迭代器會返回每個獨立的鍵
let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for (let key of colors.keys()) {
  console.log(key);
}
for (let key of tracking.keys()) {
  console.log(key);
}
for (let key of data.keys()) {
  console.log(key);
}
  • 調(diào)用console.log()方法后輸出以下內(nèi)容
0
1
2
1234
5678
9012
"title"
"format"
  • keys()迭代器會獲取colors蝗锥、trackingdata這3個集合中的每一個鍵,而且分別在3個for-of循環(huán)內(nèi)部將這些鍵名打印出來率触。對于數(shù)組對象來說终议,無論是否為數(shù)組添加命名屬性,打印出來的都是數(shù)字類型的索引葱蝗;而for-in循環(huán)迭代的是數(shù)組屬性而不是數(shù)字類型的索引

不同集合類型的默認(rèn)迭代器

每個集合類型都有一個默認(rèn)的迭代器穴张,在for-of循環(huán)中,如果沒有顯式指定則使用默認(rèn)的迭代器两曼。數(shù)組和Set集合的默認(rèn)迭代器是values()方法皂甘,Map集合的默認(rèn)迭代器是entries()方法。有了這些默認(rèn)的迭代器悼凑,可以更輕松地在for-of循環(huán)中使用集合對象

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "print");
// 與使用 colors.values() 相同
for (let value of colors) {
  console.log(value);
}
// 與使用 tracking.values() 相同
for (let num of tracking) {
  console.log(num);
}
// 與使用 data.entries() 相同
for (let entry of data) {
  console.log(entry);
}
  • 上述代碼未指定迭代器偿枕,所以將使用默認(rèn)的迭代器璧瞬。數(shù)組、Set集合及Map集合的默認(rèn)迭代器也會反應(yīng)出這些對象的初始化過程渐夸,所以這段代碼會輸出以下內(nèi)容
"red"
"green"
"blue"
1234
5678
9012["title", "Understanding ES6"]
["format", "print"]
  • 默認(rèn)情況下嗤锉,如果是數(shù)組和Set集合,會逐一返回集合中所有的值墓塌。如果是Map集合瘟忱,則按照Map構(gòu)造函數(shù)參數(shù)的格式返回相同的數(shù)組內(nèi)容。而WeakSet集合與WeakMap集合就沒有內(nèi)建的迭代器苫幢,由于要管理弱引用访诱,因而無法確切地知道集合中存在的值,也就無法迭代這些集合了

【字符串迭代器】

ES5發(fā)布以后韩肝,JS字符串慢慢變得更像數(shù)組了触菜,例如,ES5正式規(guī)定可以通過方括號訪問字符串中的字符(也就是說伞梯,text[0]可以獲取字符串text的第一個字符玫氢,并以此類推)。由于方括號操作的是編碼單元而非字符谜诫,因此無法正確訪問雙字節(jié)字符

var message = "A ?? B" ;
for (let i=0; i < message.length; i++) {
  console.log(message[i]);
}
  • 在這段代碼中漾峡,訪問messagelength屬性獲取索引值,并通過方括號訪問來迭代并打印一個單字符字符串喻旷,但是輸出的結(jié)果卻與預(yù)期不符
A

B
  • 由于雙字節(jié)字符被視作兩個獨立的編碼單元生逸,從而最終在AB之間打印出4個空行

  • 所幸,ES6的目標(biāo)是全面支持Unicode且预,并且我們可以通過改變字符串的默認(rèn)迭代器來解決這個問題槽袄,使其操作字符而不是編碼單元。現(xiàn)在锋谐,修改前一個示例中字符串的默認(rèn)迭代器遍尺,讓for-of循環(huán)輸出正確的內(nèi)容

var message = "A ?? B" ;
for (let c of message) {
  console.log(c);
}
  • 這段代碼輸出以下內(nèi)容
A

??

B
  • 這個結(jié)果更符合預(yù)期,通過循環(huán)語句可以直接操作字符并成功打印出Unicode字符

【NodeList迭代器】

DOM標(biāo)準(zhǔn)中有一個NodeList類型涮拗,document對象中的所有元素都用這個類型來表示乾戏。對于編寫Web瀏覽器環(huán)境中的JS開發(fā)者來說,需要花點兒功夫去理解NodeList對象和數(shù)組之間的差異三热。二者都使用length屬性來表示集合中元素的數(shù)量鼓择,都可以通過方括號來訪問集合中的獨立元素。而在內(nèi)部實現(xiàn)中就漾,二者的表現(xiàn)非常不一致呐能,因而會造成很多困擾

  • 自從ES6添加了默認(rèn)迭代器后,DOM定義中的NodeList類型(定義在HTML標(biāo)準(zhǔn)而不是ES6標(biāo)準(zhǔn)中)也擁有了默認(rèn)迭代器抑堡,其行為與數(shù)組的默認(rèn)迭代器完全一致摆出。所以可以將NodeList應(yīng)用于for-of循環(huán)及其他支持對象默認(rèn)迭代器的地方
var divs = document.getElementsByTagName("div");
for (let div of divs) {
  console.log(div.id);
}
  • 在這段代碼中朗徊,通過調(diào)用getElementsByTagName()方法獲取到document對象中所有div元素的列表,在for-of循環(huán)中遍歷列表中的每一個元素并輸出元素ID懊蒸,實際上是按照處理數(shù)組的方式來處理NodeList

高級迭代器

迭代器的基礎(chǔ)功能可以輔助完成很多任務(wù)荣倾,通過生成器創(chuàng)建迭代器的過程也很便捷,除了這些簡單的集合遍歷任務(wù)之外骑丸,迭代器也可以被用于完成一些復(fù)雜的任務(wù)

【給迭代器傳遞參數(shù)】

迭代器既可以用迭代器的next()方法返回值,也可以在生成器內(nèi)部使用yield關(guān)鍵字來生成值妒貌。如果給迭代器的next()方法傳遞參數(shù)通危,則這個參數(shù)的值就會替代生成器內(nèi)部上條yield語句的返回值。而如果要實現(xiàn)更多像異步編程這樣的高級功能灌曙,那么這種給迭代器傳值的能力就變得至關(guān)重要

function *createIterator() {
  let first = yield 1;
  let second = yield first + 2; // 4 + 2
  yield second + 3; // 5 + 3
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.next(5)); // "{ value: 8, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
  • 第一次調(diào)用next()方法時無論傳入什么參數(shù)都會被丟棄菊碟。由于傳給next()方法的參數(shù)會替代上一次yield的返回值,而在第一次調(diào)用next()方法前不會執(zhí)行任何yield語句在刺,因此在第一次調(diào)用next()方法時傳遞參數(shù)是毫無意義的

  • 第二次調(diào)用next()方法傳入數(shù)值4作為參數(shù)逆害,它最后被賦值給生成器函數(shù)內(nèi)部的變量first。在一個含參yield語句中蚣驼,表達式右側(cè)等價于第一次調(diào)用next()方法后的下一個返回值魄幕,表達式左側(cè)等價于第二次調(diào)用next()方法后,在函數(shù)繼續(xù)執(zhí)行前得到的返回值颖杏。第二次調(diào)用next()方法傳入的值為4纯陨,它會被賦值給變量first,函數(shù)則繼續(xù)執(zhí)行留储。第二條yield語句在第一次yield的結(jié)果上加了2翼抠,最終的返回值為6

  • 第三次調(diào)用next()方法時,傳入數(shù)值5获讳,這個值被賦值給second阴颖,最后用于第三條yield語句并最終返回數(shù)值8

【在迭代器中拋出錯誤】

除了給迭代器傳遞數(shù)據(jù)外,還可以給它傳遞錯誤條件丐膝。通過throw()方法量愧,當(dāng)?shù)骰謴?fù)執(zhí)行時可令其拋出一個錯誤。這種主動拋出錯誤的能力對于異步編程而言至關(guān)重要尤误,也能提供模擬結(jié)束函數(shù)執(zhí)行的兩種方法(返回值或拋出錯誤)侠畔,從而增強生成器內(nèi)部的編程彈性。將錯誤對象傳給throw()方法后损晤,在迭代器繼續(xù)執(zhí)行時其會被拋出

function *createIterator() {
  let first = yield 1;
  let second = yield first + 2; // yield 4 + 2 软棺,然后拋出錯誤
  yield second + 3; // 永不會被執(zhí)行
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // 從生成器中拋出了錯誤
  • 在這個示例中,前兩個表達式正常求值尤勋,而調(diào)用throw()方法后喘落,在繼續(xù)執(zhí)行let
    second求值前茵宪,錯誤就會被拋出并阻止了代碼繼續(xù)執(zhí)行。這個過程與直接拋出錯誤很相似瘦棋,二者唯一的區(qū)別是拋出的時機不同

  • 可以在生成器內(nèi)部通過try-catch代碼塊來捕獲這些錯誤

function *createIterator() {
  let first = yield 1;
  let second;    try {
    second = yield first + 2; // yield 4 + 2 稀火,然后拋出錯誤
  } 
  catch (ex) {
    second = 6; // 當(dāng)出錯時,給變量另外賦值 
  }
  yield second + 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
  • 在此示例中赌朋,try-catch代碼塊包裹著第二條yield語句凰狞。盡管這條語句本身沒有錯誤,但在給變量second賦值前還是會主動拋出錯誤沛慢,catch代碼塊捕獲錯誤后將second變量賦值為6赡若,下一條yield語句繼續(xù)執(zhí)行后返回9

  • 這里有一個有趣的現(xiàn)象調(diào)用throw()方法后也會像調(diào)用next()方法一樣返回一個結(jié)果對象。由于在生成器內(nèi)部捕獲了這個錯誤团甲,因而會繼續(xù)執(zhí)行下一條yield語句逾冬,最終返回數(shù)值9

  • 如此一來,next()throw()就像是迭代器的兩條指令躺苦,調(diào)用next()方法命令迭代器繼續(xù)執(zhí)行(可能提供一個值)身腻,調(diào)用throw()方法也會命令迭代器繼續(xù)執(zhí)行,但同時也拋出一個錯誤匹厘,在此之后的執(zhí)行過程取決于生成器內(nèi)部的代碼

  • 在迭代器內(nèi)部嘀趟,如果使用了yield語句,則可以通過next()方法和throw()方法控制執(zhí)行過程集乔,當(dāng)然去件,也可以使用return語句返回一些與普通函數(shù)返回語句不太一樣的內(nèi)容

【生成器返回語句】

由于生成器也是函數(shù),因此可以通過return語句提前退出函數(shù)執(zhí)行扰路,對于最后一次next()方法調(diào)用尤溜,可以主動為其指定一個返回值。正如在其他函數(shù)中那樣汗唱,可以通過return語句指定一個返回值宫莱。而在生成器中,return表示所有操作已經(jīng)完成哩罪,屬性done被設(shè)置為true授霸;如果同時提供了相應(yīng)的值,則屬性value會被設(shè)置為這個值

function *createIterator() {
  yield 1;    
  return;
  yield 2;
  yield 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
  • 這段代碼中的生成器包含多條yield語句和一條return語句际插,其中return語句緊隨第一條yield語句碘耳,其后的yield語句將不會被執(zhí)行

  • return語句中也可以指定一個返回值,該值將被賦值給返回對象的value屬性

function *createIterator() {
  yield 1;    
  return 42;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 42, done: true }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
  • 在此示例中框弛,第二次調(diào)用next()方法時返回對象的value屬性值為42辛辨,done屬性首次設(shè)為true;第三次調(diào)用next()方法依然返回一個對象,只是value屬性的值會變?yōu)?code>undefined斗搞。因此指攒,通過return語句指定的返回值,只會在返回對象中出現(xiàn)一次僻焚,在后續(xù)調(diào)用返回的對象中允悦,value屬性會被重置為undefined

[注意]展開運算符與for-of循環(huán)語句會直接忽略通過return語句指定的任何返回值,只要done一變?yōu)?code>true就立即停止讀取其他的值虑啤。不管怎樣隙弛,迭代器的返回值依然是一個非常有用的特性

【委托生成器】

在某些情況下,我們需要將兩個迭代器合二為一狞山,這時可以創(chuàng)建一個生成器驶鹉,再給yield語句添加一個星號,就可以將生成數(shù)據(jù)的過程委托給其他生成器铣墨。當(dāng)定義這些生成器時,只需將星號放置在關(guān)鍵字yield和生成器的函數(shù)名之間即可

function *createNumberIterator() {
  yield 1;
  yield 2;
}
function *createColorIterator() {
  yield "red";
  yield "green";
}
function *createCombinedIterator() {
  yield *createNumberIterator();
  yield *createColorIterator();
  yield true;
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "red", done: false }"
console.log(iterator.next()); // "{ value: "green", done: false }"
console.log(iterator.next()); // "{ value: true, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
  • 這里的生成器createCombinedIterator()先后委托了另外兩個生成器createNumberlterator()createColorlterator()办绝。僅根據(jù)迭代器的返回值來看伊约,它就像是一個完整的迭代器,可以生成所有的值孕蝉。每一次調(diào)用next()方法就會委托相應(yīng)的迭代器生成相應(yīng)的值屡律,直到最后由createNumberlterator()cpeateColorlterator()創(chuàng)建的迭代器無法返回更多的值,此時執(zhí)行最后一條yield語句并返回true

  • 有了生成器委托這個新功能降淮,可以進一步利用生成器的返回值來處理復(fù)雜任務(wù)

function *createNumberIterator() {
  yield 1;
  yield 2;    
  return 3;
}
function *createRepeatingIterator(count) {    
  for (let i=0; i < count; i++) {
    yield "repeat";
  }
}
function *createCombinedIterator() {
  let result = yield *createNumberIterator();
  yield *createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
  • 在生成器createCombinedlterator()中超埋,執(zhí)行過程先被委托給了生成器createNumberlterator(),返回值會被賦值給變量result佳鳖,執(zhí)行到return 3時會返回數(shù)值3霍殴。這個值隨后被傳入createRepeatinglterator()作為它的參數(shù),因而生成字符串"repeat"yield語句會被執(zhí)行三次

  • 無論通過何種方式調(diào)用迭代器next()方法系吩,數(shù)值3都不會被返回来庭,它只存在于生成器createCombinedlterator()的內(nèi)部。但如果想輸出這個值穿挨,則可以額外添加一條yield語句

function *createNumberIterator() {
  yield 1;
  yield 2;    
  return 3;
}
function *createRepeatingIterator(count) {    
  for (let i=0; i < count; i++) {
    yield "repeat";
  }
}
function *createCombinedIterator() {
  let result = yield *createNumberIterator();
  yield result;
  yield *createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
  • 此處新添加的yield語句顯式地輸出了生成器createNumberlterator()的返回值月弛。

[注意]yield*也可直接應(yīng)用于字符串,例如yield* "hello"科盛,此時將使用字符串的默認(rèn)迭代器

異步任務(wù)執(zhí)行

生成器令人興奮的特性多與異步編程有關(guān)帽衙,JS中的異步編程有利有弊:簡單任務(wù)的異步化非常容易;而復(fù)雜任務(wù)的異步化會帶來很多管理代碼的挑戰(zhàn)贞绵。由于生成器支持在函數(shù)中暫停代碼執(zhí)行厉萝,因而可以深入挖掘異步處理的更多用法

  • 執(zhí)行異步操作的傳統(tǒng)方式一般是調(diào)用一個函數(shù)并執(zhí)行相應(yīng)回調(diào)函數(shù)
let fs = require("fs");
fs.readFile("config.json", function(err, contents) {    
  if (err) {
    throw err;
  }
  doSomethingWith(contents);
  console.log("Done");
});
  • 調(diào)用fs.readFile()方法時要求傳入要讀取的文件名和一個回調(diào)函數(shù),操作結(jié)束后會調(diào)用該回調(diào)函數(shù)并檢查是否存在錯誤,如果沒有就可以處理返回的內(nèi)容。如果要執(zhí)行的任務(wù)很少,那么這樣的方式可以很好地完成任務(wù)漓藕;如若需要嵌套回調(diào)或序列化一系列的異步操作嚎幸,事情會變得非常復(fù)雜。此時花沉,生成器和yield語句就派上用場了

【簡單任務(wù)執(zhí)行器】

由于執(zhí)行yield語句會暫停當(dāng)前函數(shù)的執(zhí)行過程并等待下一次調(diào)用next()方法,因此可以創(chuàng)建一個函數(shù),在函數(shù)中調(diào)用生成器生成相應(yīng)的迭代器舞肆,從而在不用回調(diào)函數(shù)的基礎(chǔ)上實現(xiàn)異步調(diào)用next()方法

function run(taskDef) {
  // 創(chuàng)建迭代器,讓它在別處可用
  let task = taskDef();// 啟動任務(wù)
  let result = task.next();// 遞歸使用函數(shù)來保持對 next() 的調(diào)用
  function step() {
    // 如果還有更多要做的
    if (!result.done) {
      result = task.next();
      step();
    }
  }
  // 開始處理過程 step();
}
  • 函數(shù)run()接受一個生成器函數(shù)作為參數(shù)博杖,這個函數(shù)定義了后續(xù)要執(zhí)行的任務(wù)椿胯,生成一個迭代器并將它儲存在變量task中。首次調(diào)用迭代器的next()方法時剃根,返回的結(jié)果被儲存起來稍后繼續(xù)使用哩盲。step()函數(shù)會檢查result.done的值,如果為false則執(zhí)行迭代器的next()方法狈醉,并再次執(zhí)行step()操作廉油。每次調(diào)用next()方法時,返回的最新信息總會覆寫變量result苗傅。在代碼的最后抒线,初始化執(zhí)行step()函數(shù)并開始整個的迭代過程,每次通過檢查result.done來確定是否有更多任務(wù)需要執(zhí)行

  • 借助這個run()函數(shù)渣慕,可以像這樣執(zhí)行一個包含多條yield語句的生成器

run(function*() {
  console.log(1);
  yield;
  console.log(2);
  yield;
  console.log(3);
});
  • 這個示例最終會向控制臺輸出多次調(diào)用next()方法的結(jié)果嘶炭,分別為數(shù)值1、2和3逊桦。當(dāng)然眨猎,簡單輸出迭代次數(shù)不足以展示迭代器高級功能的實用之處,下一步將在迭代器與調(diào)用者之間互相傳值

【向任務(wù)執(zhí)行器傳遞數(shù)據(jù)】

給任務(wù)執(zhí)行器傳遞數(shù)據(jù)的最簡單辦法是卫袒,將值通過迭代器的next()方法傳入作為yield的生成值供下次調(diào)用宵呛。在這段代碼中,只需將result.value傳入next()方法即可

function run(taskDef) {// 創(chuàng)建迭代器夕凝,讓它在別處可用
  let task = taskDef();// 啟動任務(wù)
  let result = task.next();// 遞歸使用函數(shù)來保持對 next() 的調(diào)用
  function step() {
  // 如果還有更多要做的
    if (!result.done) {
      result = task.next(result.value);
        step();
      }
    }
    // 開始處理過程 step();
}
  • 現(xiàn)在result.value作為next()方法的參數(shù)被傳入宝穗,這樣就可以在yield調(diào)用之間傳遞數(shù)據(jù)了
run(function*() {
  let value = yield 1;
  console.log(value); // 1
  value = yield value + 3;
  console.log(value); // 4
});
  • 此示例會向控制臺輸出兩個數(shù)值1和4。其中码秉,數(shù)值1取自yield 1語句中回傳給變量value的值逮矛;而4取自給變量value加3后回傳給value的值。現(xiàn)在數(shù)據(jù)已經(jīng)能夠在yield調(diào)用間互相傳遞了转砖,只需一個小小改變便能支持異步調(diào)用

【異步任務(wù)執(zhí)行器】

之前的示例只是在多個yield調(diào)用間來回傳遞靜態(tài)數(shù)據(jù)须鼎,而等待一個異步過程有些不同鲸伴。任務(wù)執(zhí)行器需要知曉回調(diào)函數(shù)是什么以及如何使用它。由于yield表達式會將值返回給任務(wù)執(zhí)行器晋控,所有的函數(shù)調(diào)用都會返回一個值汞窗,因而在某種程度上這也是一個異步操作,任務(wù)執(zhí)行器會一直等待直到操作完成

  • 下面定義一個異步操作
function fetchData() {
  return function(callback) {
    callback(null, "Hi!");
  };
}
  • 本示例的原意是讓任務(wù)執(zhí)行器調(diào)用的所有函數(shù)都返回一個可以執(zhí)行回調(diào)過程的函數(shù)赡译,此處fetchData()函數(shù)的返回值是一個可接受回調(diào)函數(shù)作為參數(shù)的函數(shù)仲吏,當(dāng)調(diào)用它時會傳入一個字符串"Hi!"作為回調(diào)函數(shù)的參數(shù)并執(zhí)行。參數(shù)callback需要通過任務(wù)執(zhí)行器指定蝌焚,以確惫簦回調(diào)函數(shù)執(zhí)行時可以與底層迭代器正確交互。盡管fetchData()是同步函數(shù)只洒,但簡單添加一個延遲方法即可將其變?yōu)楫惒胶瘮?shù)
function fetchData() {
  return function(callback) {
    setTimeout(function() {
      callback(null, "Hi!");
    }, 50);
  };
}
  • 在這個版本的fetchData()函數(shù)中许帐,讓回調(diào)函數(shù)延遲了50ms再被調(diào)用,所以這種模式在同步和異步狀態(tài)下都運行良好毕谴。只需保證每個要通過yield關(guān)鍵字調(diào)用的函數(shù)都按照與之相同的模式編寫

  • 理解了函數(shù)中異步過程的運作方式成畦,可以將任務(wù)執(zhí)行器稍作修改。當(dāng)result.value是一個函數(shù)時涝开,任務(wù)執(zhí)行器會先執(zhí)行這個函數(shù)再將結(jié)果傳入next()方法

function run(taskDef) {
  // 創(chuàng)建迭代器羡鸥,讓它在別處可用
  let task = taskDef();// 啟動任務(wù)
  let result = task.next();// 遞歸使用函數(shù)來保持對 next() 的調(diào)用
  function step() {
    // 如果還有更多要做的
    if (!result.done) {            
      if (typeof result.value === "function") {
        result.value(function(err, data) {                    
          if (err) {
            result = task.throw(err);                        
            return;
          }
          result = task.next(data);
          step();
        });
      } else {
        result = task.next(result.value);
        step();
      }
    }
  }
  // 開始處理過程 step();
}
  • 通過===操作符檢査后,如果result.value是一個函數(shù)忠寻,會傳入一個回調(diào)函數(shù)作為參數(shù)調(diào)用它,回調(diào)函數(shù)遵循Node.js有關(guān)執(zhí)行錯誤的約定:所有可能的錯誤放在第一個參數(shù)(err)中存和,結(jié)果放在第二個參數(shù)中奕剃。如果傳入了err,意味著執(zhí)行過程中產(chǎn)生了錯誤捐腿,這時通過task.throw()正確輸出錯誤對象纵朋;如果沒有錯誤產(chǎn)生,data被傳入task.next()作為結(jié)果儲存起來茄袖,并繼續(xù)執(zhí)行step()操软。如果result.value不是一個函數(shù),則直接將其傳入next()方法

  • 現(xiàn)在宪祥,這個新版的任務(wù)執(zhí)行器已經(jīng)可以用于所有的異步任務(wù)了聂薪。在Node.js環(huán)境中,如果要從文件中讀取一些數(shù)據(jù)蝗羊,需要在fs.readFile()外圍創(chuàng)建一個包裝器(wrapper)藏澳,并返回一個與fetchData()類似的函數(shù)

let fs = require("fs");    
function readFile(filename) {
  return function(callback) {
    fs.readFile(filename, callback);
  };
}
  • readFile()接受一個文件名作為參數(shù),返回一個可以執(zhí)行回調(diào)函數(shù)的函數(shù)耀找∠栌疲回調(diào)函數(shù)被直接傳入fs.readFile()方法,讀取完成后會執(zhí)行它
run(function*() {
  let contents = yield readFile("config.json");
  doSomethingWith(contents);
  console.log("Done");
});
  • 在這段代碼中沒有任何回調(diào)變量,異步的readFile()操作卻正常執(zhí)行蓄愁,除了yield關(guān)鍵字外双炕,其他代碼與同步代碼完全一樣,只不過函數(shù)執(zhí)行的是異步操作撮抓。所以遵循相同的接口妇斤,可以編寫一些讀起來像是同步代碼的異步邏輯

  • 當(dāng)然,這些示例中使用的模式也有缺點胀滚,也就是不能百分百確認(rèn)函數(shù)中返回的其他函數(shù)一定是異步的趟济。著眼當(dāng)下,最重要的是能理解任務(wù)執(zhí)行過程背后的理論知識

其他章節(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末咽笼,一起剝皮案震驚了整個濱河市顷编,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌剑刑,老刑警劉巖媳纬,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異施掏,居然都是意外死亡钮惠,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門七芭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來素挽,“玉大人,你說我怎么就攤上這事狸驳≡っ鳎” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵耙箍,是天一觀的道長撰糠。 經(jīng)常有香客問我,道長辩昆,這世上最難降的妖魔是什么阅酪? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮汁针,結(jié)果婚禮上术辐,老公的妹妹穿的比我還像新娘。我一直安慰自己施无,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布帆精。 她就那樣靜靜地躺著较屿,像睡著了一般隧魄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上隘蝎,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天购啄,我揣著相機與錄音,去河邊找鬼嘱么。 笑死狮含,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的曼振。 我是一名探鬼主播几迄,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼冰评!你這毒婦竟也來了映胁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤甲雅,失蹤者是張志新(化名)和其女友劉穎解孙,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抛人,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡弛姜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了妖枚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片廷臼。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖绝页,靈堂內(nèi)的尸體忽然破棺而出中剩,到底是詐尸還是另有隱情,我是刑警寧澤屈芜,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布井佑,位于F島的核電站躬翁,受9級特大地震影響盒发,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拼卵,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一蛮艰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧即寡,春花似錦聪富、人聲如沸善涨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽膜钓。三九已至颂斜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間梅肤,已是汗流浹背俊啼。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工授帕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留豪墅,地道東北人偶器。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓颊郎,卻偏偏與公主長得像姆吭,于是被迫代替她去往敵國和親内狸。 傳聞我的和親對象是個殘疾皇子昆淡,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

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