你不懂JS:作用域與閉包 第三章:函數與塊兒作用域

官方中文版原文鏈接

感謝社區中各位的大力支持,譯者再次奉上一點點福利:阿里云產品券,享受所有官網優惠,并抽取幸運大獎:點擊這里領取

正如我們在第二章中探索的,作用域由一系列“氣泡”組成,這些“氣泡”的每一個就像一個容器或籃子,標識符(變量,函數)就在它里面被聲明。這些氣泡整齊地互相嵌套在一起,而且這種嵌套是在編寫時定義的。

但是到底是什么才能制造一個新氣泡?只能是函數嗎?JavaScript中的其他結構可以創建作用域的氣泡嗎?

函數中的作用域

對這些問題的最常見的回答是,JavaScript擁有基于函數的作用域。也就是,你聲明的每一個函數都為自己創建了一個氣泡,而且沒有其他的結構可以創建它們自己的作用域氣泡。但是就像我們一會兒就會看到的,這不完全正確。

但首先,讓我們探索一下函數作用域和它的含義。

考慮這段代碼:

function foo(a) {
    var b = 2;

    // 一些代碼

    function bar() {
        // ...
    }

    // 更多代碼

    var c = 3;
}

在這個代碼段中,foo(..)的作用域氣泡包含標識符ab,cbar。一個聲明出現在作用域 何處無關緊要的,不管怎樣,變量和函數屬于包含它們的作用域氣泡。在下一章中我們將會探索這到底是如何工作的。

bar(..)擁有它自己的作用域氣泡。全局作用域也一樣,它僅含有一個標識符:foo

因為abc,和bar都屬于foo(..)的作用域氣泡,所以它們在foo(..)外部是不可訪問的。也就是,接下來的代碼都會得到ReferenceError錯誤,因為這些標識符在全局作用域中都不可用:

bar(); // 失敗

console.log( a, b, c ); // 3個都失敗

然而,所有這些標識符(a,b,c,和bar)在foo(..)內部 都是可以訪問的,而且在bar(..)內部實際上也都是可用的(假定在bar(..)內部沒有遮蔽標識符的聲明)。

函數作用域支持著這樣的想法:所有變量都屬于函數,而且貫穿整個函數始終都可以使用和重用(而且甚至可以在嵌套的作用域中訪問)。這種設計方式可以十分有用,而且肯定可以完全利用JavaScript的“動態”性質 —— 變量可以根據需要接受不同種類型的值。

另一方面,如果你不小心提防,跨越整個作用域存在的變量可能會導致一些以外的陷阱。

隱藏于普通作用域

考慮一個函數的傳統方式是,你聲明一個函數,并在它內部添加代碼。但是相反的想法也同樣強大和有用:拿你所編寫的代碼的任意一部分,在它周圍包裝一個函數聲明,這實質上“隱藏”了這段代碼。

其實際結果是在這段代碼周圍創建了一個作用域氣泡,這意味著現在在這段代碼中的任何聲明都將綁在這個新的包裝函數的作用域上,而不是前一個包含它們的作用域。換句話說,你可以通過將變量和函數圍在一個函數的作用域中來“隱藏”它們。

為什么“隱藏”變量和函數是一種有用的技術?

有各種原因驅使著這種基于作用域的隱藏。它們主要是由一種稱為“最低權限原則”的軟件設計原則引起的[1],有時也被稱為“最低授權”或“最少曝光”。這個原則規定,在軟件設計中,比如一個模塊/對象的API,你應當只暴露所需要的最低限度的東西,而“隱藏”其他的一切。

這個原則可以擴展到用哪個作用域來包含變量和函數的選擇。如果所有的變量和函數在全局作用域中,它們將理所當然地對任何嵌套的作用域是可訪問的。但這回違背“最少……”原則,因為你(很可能)暴露了許多你本應當保持為私有的變量和函數,而這些代碼的恰當用法是不鼓勵訪問這些變量/函數的。

例如:

function doSomething(a) {
    b = a + doSomethingElse( a * 2 );

    console.log( b * 3 );
}

function doSomethingElse(a) {
    return a - 1;
}

var b;

doSomething( 2 ); // 15

在這個代碼段中,變量b和函數doSomethingElse(..)很可能是doSomething(..)如何工作的“私有”細節。允許外圍的作用域“訪問”bdoSomethingElse(..)不僅沒必要而且可能是“危險的”,因為它們可能會以種種意外的方式,有意或無意地被使用,而這也許違背了doSomething(..)假設的前提條件。

一個更“恰當”的設計是講這些私有細節隱藏在doSomething(..)的作用域內部,比如:

function doSomething(a) {
    function doSomethingElse(a) {
        return a - 1;
    }

    var b;

    b = a + doSomethingElse( a * 2 );

    console.log( (b * 3) );
}

doSomething( 2 ); // 15

現在,bdoSomethingElse(..)對任何外界影響都是不可訪問的,而是僅僅由doSomething(..)控制。它的功能和最終結果不受影響,但是這種設計將私有細節保持為私有的,這通常被認為是好的軟件。

避免沖突

將變量和函數“隱藏”在一個作用域內部的另一個好處是,避免兩個同名但用處不同的標識符之間發生無意的沖突。沖突經常導致值被意外地覆蓋。

例如:

function foo() {
    function bar(a) {
        i = 3; // 在外圍的for循環的作用域中改變`i`
        console.log( a + i );
    }

    for (var i=0; i<10; i++) {
        bar( i * 2 ); // 噢,無限循環!
    }
}

foo();

bar(..)內部的賦值i = 3意外地覆蓋了在foo(..)的for循環中聲明的i。在這個例子中,這將導致一個無限循環,因為i被設定為固定的值3,而它將永遠< 10

bar(..)內部的賦值需要聲明一個本地變量來使用,不論選用什么樣的標識符名稱。var i = 3;將修復這個問題(并將為i創建一個前面提到的“遮蔽變量”聲明)。一個 另外的,不是代替的,選項是完全選擇另外一個標識符名稱,比如var j = 3;。但是你的軟件設計也許會自然而然地使用相同的標識符名稱,所以在這種情況下利用作用域來“隱藏”你的內部聲明是你最好/唯一的選擇。

全局“名稱空間”

變量沖突(可能)發生的一個特別強有力的例子是在全局作用域中。多個庫被加載到你的程序中時可以十分容易地互相沖突,如果它們沒有適當地隱藏它們的內部/私有函數和變量。

這樣的庫通常會在全局作用域中使用一個足夠獨特的名稱來創建一個單獨的變量聲明,它經常是一個對象。然后這個對象被用作這個庫的一個“名稱空間”,所有要明確暴露出來的功能都被作為屬性掛在這個對象(名稱空間)上,而不是將它們自身作為頂層詞法作用域的標識符。

例如:

var MyReallyCoolLibrary = {
    awesome: "stuff",
    doSomething: function() {
        // ...
    },
    doAnotherThing: function() {
        // ...
    }
};

模塊管理

另一種回避沖突的選擇是更加現代的“模塊”方式,它使用任意一種依賴管理器。使用這些工具,沒有庫可以向全局作用域添加任何標識符,取而代之的是使用依賴管理器的各種機制,要求庫的標識符被明確地導入到另一個指定的作用域中。

應該可以看到,這些工具并不擁有可以豁免于詞法作用域規則的“魔法”功能。它們簡單地使用這里講解的作用域規則,來強制標識符不會被注入任何共享的作用域,而是保持在私有的,不易沖突的作用域中,這防止了任何意外的作用域沖突。

因此,如果你選擇這樣做的話,你可以防御性地編碼,并在實際上不使用依賴管理器的情況下,取得與使用它們相同的結果。關于模塊模式的更多信息參見第五章。

函數作為作用域

我們已經看到,我們可以拿來一段代碼并在它周圍包裝一個函數,而這實質上對外部作用域“隱藏”了這個函數內部作用域包含的任何變量或函數聲明。

例如:

var a = 2;

function foo() { // <-- 插入這個

    var a = 3;
    console.log( a ); // 3

} // <-- 和這個
foo(); // <-- 還有這個

console.log( a ); // 2

雖然這種技術“可以工作”,但它不一定非常理想。它引入了幾個問題。首先是我們不得不聲明一個命名函數foo(),這意味著這個標識符名稱foo本身就“污染”了外圍作用域(在這個例子中是全局)。我們要不得不通過名稱(foo())明確地調用這個函數來使被包裝的代碼真正運行。

如果這個函數不需要名稱(或者,這個名稱不污染外圍作用域),而且如果這個函數能自動地被執行就更理想了。

幸運的是,JavaScript給這兩個問題提供了一個解決方法。

var a = 2;

(function foo(){ // <-- 插入這個

    var a = 3;
    console.log( a ); // 3

})(); // <-- 和這個

console.log( a ); // 2

讓我們分析一下這里發生了什么。

首先注意,與僅僅是function...相對,這個包裝函數語句以(function...)開頭。雖然這看起來像是一個微小的細節,但實際上這是重大改變。與將這個函數視為一個標準的聲明不同的是,這個函數被視為一個函數表達式。

注意: 區分聲明與表達式的最簡單的方法是,這個語句中(不僅僅是一行,而是一個獨立的語句)“function”一詞的位置。如果“function”是這個語句中的第一個東西,那么它就是一個函數聲明。否則,它就是一個函數表達式。

這里我們可以觀察到一個函數聲明和一個函數表達式之間的關鍵不同是,它的名稱作為一個標識符被綁定在何處。

比較這前兩個代碼段。在第一個代碼段中,名稱foo被綁定在外圍作用域中,我們用foo()直接調用它。在第二個代碼段中,名稱foo沒有被綁定在外圍作用域中,而是被綁定在它自己的函數內部。

換句話說,(function foo(){ .. })作為一個表達式意味著標識符foo僅能在..代表的作用域中被找到,而不是在外部作用域中。將名稱foo隱藏在它自己內部意味著它不會多余地污染外圍作用域。

匿名與命名

你可能對函數表達式作為回調參數再熟悉不過了,比如:

setTimeout( function(){
    console.log("I waited 1 second!");
}, 1000 );

這稱為一個“匿名函數表達式”,因為function()...上沒有名稱標識符。函數表達式可以是匿名的,但是函數聲明不能省略名稱 —— 那將是不合法的JS程序。

匿名函數表達式可以快速和很容易地鍵入,而且許多庫和工具往往鼓勵使用這種代碼慣用風格。然而,它們有幾個缺點需要考慮:

  1. 在棧軌跡上匿名函數沒有有用的名稱可以表示,這使得調試更加困難。

  2. 沒有名稱的情況下,如果這個函數需要為了遞歸等目的引用它自己,那么就需要很不幸地使用 被廢棄的 arguments.callee引用。另一個需要自引用的例子是,當一個事件處理器函數在被觸發后想要把自己解除綁定。

  3. 匿名函數省略的名稱經常對提供更易讀/易懂的代碼很有幫助。一個描述性的名稱可以幫助代碼自解釋。

內聯函數表達式 很強大且很有用 —— 匿名和命名的問題并不會貶損這一點。給你的函數表達式提供一個名稱就可以十分有效地解決這些缺陷,而且沒有實際的壞處。最佳的方法是總是命名你的函數表達式:

setTimeout( function timeoutHandler(){ // <-- 看,我有一個名字!
    console.log( "I waited 1 second!" );
}, 1000 );

立即調用函數表達式

var a = 2;

(function foo(){

    var a = 3;
    console.log( a ); // 3

})();

console.log( a ); // 2

得益于包裝在一個()中,我們有了一個作為表達式的函數,我們可以通過在末尾加入另一個()來執行這個函數,就像(function foo(){ .. })()。第一個外圍的()使這個函數變成表達式,而第二個()執行這個函數。

這個模式是如此常見,以至于幾年前開發者社區同意給它一個術語:IIFE,它表示“立即被調用的函數表達式”(Immediately Invoked Function E xpression)。

當然,IIFE不一定需要一個名稱 —— IIFE的最常見形式是使用一個匿名函數表達式。雖然少見一些,與匿名函數表達式相比,命名的IIFE擁有前述所有的好處,所以它是一個可以采用的好方式。

var a = 2;

(function IIFE(){

    var a = 3;
    console.log( a ); // 3

})();

console.log( a ); // 2

傳統的IIFE有一種稍稍變化的形式,一些人偏好:(function(){ .. }())。仔細觀察不同之處。在第一種形式中,函數表達式被包在( )中,然后用于調用的()出現在它的外側。在第二種形式中,用于調用的()被移動到用于包裝的( )內側。

這兩種形式在功能上完全相同。這純粹是一個你偏好的風格的選擇。

IIFE的另一種十分常見的變種是,利用它們實際上只是函數調用的事實,來傳入參數值。

例如:

var a = 2;

(function IIFE( global ){

    var a = 3;
    console.log( a ); // 3
    console.log( global.a ); // 2

})( window );

console.log( a ); // 2

我們傳入window對象引用,但是我們將參數命名為global,這樣我們對于全局和非全局引用就有了一個清晰的文體上的劃分。當然,你可以從外圍作用域傳入任何你想要的東西,而且你可以將參數命名為任何適合你的名稱。這幾乎僅僅是文體上的選擇。

這種模式的另一種應用解決了一個小問題:默認的undefined標識符的值也許會被不正確地覆蓋掉,而導致意外的結果。通過將參數命名為undefined,同時不為它傳遞任何參數值,我們就可以保證在一個代碼塊中undefined標識符確實是是一個未定義的值。

undefined = true; // 給其他的代碼埋地雷!別這么干!

(function IIFE( undefined ){

    var a;
    if (a === undefined) {
        console.log( "Undefined is safe here!" );
    }

})();

IIFE還有另一種變種將事情的順序倒了過來,要被執行的函數在調用和傳遞給它的參數 之后 給出。這種模式被用于UMD(Universal Module Definition —— 統一模塊定義)項目。一些人發現它更干凈和移動一些,雖然有點兒繁冗。

var a = 2;

(function IIFE( def ){
    def( window );
})(function def( global ){

    var a = 3;
    console.log( a ); // 3
    console.log( global.a ); // 2

});

def函數表達式帶這個代碼段的后半部分被定義,然后作為一個參數(也叫def)被傳遞給在代碼段前半部分定義的IIFE函數。最后,參數def(函數)被調用,并將window作為global參數傳入。

塊兒作為作用域

雖然函數是最常見的作用域單位,而且當然也是在世面上流通的絕大多數JS中最為廣泛傳播的設計方式,但是其他的作用域單位也是可能的,而且使用這些作用域單位可以導致更好,對于維護來說更干凈的代碼。

JavaScript之外的許多其他語言都支持塊兒作用域,所以有這些語言背景的開發者習慣于這種思維模式,然而那些主要在JavaScript中工作的開發者可能會發現這個概念有些陌生。

但即使你從沒用塊兒作用域的方式寫過一行代碼,你可能依然對JavaScript中這種極其常見的慣用法很熟悉:

for (var i=0; i<10; i++) {
    console.log( i );
}

我們在for循環頭的內部直接聲明了變量i,因為我們意圖很可能是僅在這個for循環內部的上下文環境中使用i,而實質上忽略了這個變量實際上將自己劃入了外圍作用域中(函數或全局)的事實。

這就是有關塊兒作用域的一切。盡可能封閉地,盡可能局部地,在變量將被使用的位置聲明它。另一個例子是:

var foo = true;

if (foo) {
    var bar = foo * 2;
    bar = something( bar );
    console.log( bar );
}

我們僅在if語句的上下文環境中使用變量bar,所以我們將它聲明在if塊兒的內部是有些道理的。然而,當使用var時,我們在何處聲明變量是無關緊要的,因為它們將總是屬于外圍作用域。這個代碼段實質上為了代碼風格的原因“假冒”了塊兒作用域,并依賴于我們要管好自己,不要在這個作用域的其他地方意外地使用bar

從將信息隱藏在函數中到將信息隱藏在我們代碼的塊兒中,塊兒作用域是一種擴展了早先的“最低 權限 暴露原則”[1]的工具。

再次考慮這個for循環的例子:

for (var i=0; i<10; i++) {
    console.log( i );
}

為什么要用僅將(或者至少是,僅 應當)在這個for循環中使用的變量i去污染一個函數的整個作用域呢?

但更重要的是,開發者們也許偏好于 檢查 他們自己來防止在變量預期的目的之外意外地(重)使用它們,例如如果你試著在錯誤的地方使用變量會導致一個未知變量的錯誤。對于變量i的塊兒作用域(如果它是可能的話)將使i僅在for循環內部可用,使得如果在函數的其他地方訪問i將導致一個錯誤。這有助于保證變量不會被糊涂地重用或者難于維護。

但是,悲慘的現實是,表面上看來,JavaScript沒有塊兒作用域的能力。

更確切地說,直到你再深入一些才有。

with

我們在第二章中學習了with。雖然它是一個使人皺眉頭的結構,但它確實是一個(一種形式的)塊兒作用域的例子,它從對象中創建的作用域僅存在于這個with語句的生命周期中,而不再外圍作用域中。

try/catch

一個鮮為人知的是事實,JavaScript在ES3中明確指出在try/catchcatch子句中聲明的變量,是屬于catch塊兒的塊兒作用域的。

例如:

try {
    undefined(); //用非法的操作強制產生一個異常!
}
catch (err) {
    console.log( err ); // 好用!
}

console.log( err ); // ReferenceError: `err` not found

如你所見,err僅存在于catch子句中,并且在你試著從其他地方引用他時拋出一個錯誤。

注意: 雖然這種行為已經被明確規定,而且對于幾乎所有的標準JS環境(也許除了老IE)來說都是成立的,但是如果你在同一個作用域中有兩個或多個catch子句,而它們又各自用相同的標識符名稱聲明了它們表示錯誤的變量時,許多linter依然會報警。實際上這不是重定義,因為這些變量都安全地位于塊兒作用域中,但是linter看起來依然,惱人地,抱怨這個事實。

為了避免這些不必要的警告,一些開發者將他們的catch變量命名為err1,err2,等等。另一些開發者干脆關閉linter對重復變量名的檢查。

catch的塊兒作用域性質看起來像是一個沒用的,只有學院派意義的事實,但是參看附錄B來了解更多它如何有用的信息。

let

至此,我們看到JavaScript僅僅有一些奇怪的小眾行為暴露了塊兒作用域功能。如果這就是我們擁有的一切,而且許多許多年以來這 確實就是 我們擁有的一切,那么塊作用域對JavaScript開發者來說就不是非常有用。

幸運的是,ES6改變了這種狀態,并引入了一個新的關鍵字let,作為另一種聲明變量的方式伴隨著var

let關鍵字將變量聲明附著在它所在的任何塊兒(通常是一個{ .. })的作用域中。換句話說,let為它的變量聲明隱含地劫持了任意塊兒的作用域。

var foo = true;

if (foo) {
    let bar = foo * 2;
    bar = something( bar );
    console.log( bar );
}

console.log( bar ); // ReferenceError

使用let將一個變量附著在一個現存的塊兒上有些隱晦。它可能會使人困惑 —— 在你開發和設計代碼時,如果你不仔細注意哪些塊兒的作用域包含了變量,并且習慣于將塊兒四處移動,將它們包進其他的塊兒中,等等。

為塊兒作用域創建明確的塊兒可以解決這些問題中的一些,使變量附著在何處更加明顯。通常來說,明確的代碼要比隱晦或微妙的代碼好。這種明確的塊兒作用域風格很容易達成,而且它與塊兒作用域在其他語言中的工作方式匹配得更自然:

var foo = true;

if (foo) {
    { // <-- 明確的塊兒
        let bar = foo * 2;
        bar = something( bar );
        console.log( bar );
    }
}

console.log( bar ); // ReferenceError

我們可以在一個語句是合法文法的任何地方,通過簡單地引入一個{ .. }來為let創建一個任意的可以綁定的塊兒。在這個例子中,我們在if語句內部制造了一個明確的塊兒,在以后的重構中將整個塊兒四處移動可能會更容易,而且不會影響外圍的if語句的位置和語義。

注意: 另一個明確表達塊兒作用域的方法,參見附錄B。

在第四章中,我們將講解提升(hoisting),它講述關于聲明在它們所出現的整個作用域中都被認為是存在的。

然而,使用let做出的聲明將 不會 在它們所出現的整個塊兒的作用域中提升。如此,直到聲明語句為止,聲明將不會“存在”于塊兒中。

{
   console.log( bar ); // ReferenceError!
   let bar = 2;
}

垃圾回收

塊兒作用域的另一個有用之處是關于閉包和釋放內存的垃圾回收。我們將簡單地在這里展示一下,但是閉包機制將在第五章中詳細講解。

考慮這段代碼:

function process(data) {
    // 做些有趣的事
}

var someReallyBigData = { .. };

process( someReallyBigData );

var btn = document.getElementById( "my_button" );

btn.addEventListener( "click", function click(evt){
    console.log("button clicked");
}, /*capturingPhase=*/false );

點擊事件的處理器回調函數click根本不 需要 someReallyBigData變量。這意味著從理論上講,在process(..)運行之后,這個消耗巨大內存的數據結構可以被作為垃圾回收。然而,JS引擎很可能(雖然這要看具體實現)將會仍然將這個結構保持一段時間,因為click函數在整個作用域上擁有一個閉包。

塊兒作用域可以解決這個問題,使引擎清楚地知道它不必再保持someReallyBigData了:

function process(data) {
    // 做些有趣的事
}

// 運行過后,任何定義在這個塊中的東西都可以消失了
{
    let someReallyBigData = { .. };

    process( someReallyBigData );
}

var btn = document.getElementById( "my_button" );

btn.addEventListener( "click", function click(evt){
    console.log("button clicked");
}, /*capturingPhase=*/false );

聲明可以將變量綁定在本地的明確的塊兒是一種強大的工具,你可以把它加入你的工具箱。

let循環

一個使let閃光的特殊例子是我們先前討論的for循環。

for (let i=0; i<10; i++) {
    console.log( i );
}

console.log( i ); // ReferenceError

在for循環頭部的let不僅將i綁定在for循環體中,而且實際上,它會對每一次循環的 迭代 重新綁定i,確保它被賦予來自上一次循環迭代末尾的值。

這是描繪這種為每次迭代進行綁定的行為的另一種方式:

{
    let j;
    for (j=0; j<10; j++) {
        let i = j; // 每次迭代都重新綁定
        console.log( i );
    }
}

這種為每次迭代進行的綁定有趣的原因將在第五章中我們討論閉包時變得明朗。

因為let聲明附著于任意的塊兒,而不是外圍的函數作用域(或全局),所以在重構代碼時可能會有一些坑需要額外小心:現存的代碼擁有對函數作用域的var聲明有隱藏的依賴,但你想要用let來取代var。

考慮如下代碼:

var foo = true, baz = 10;

if (foo) {
    var bar = 3;

    if (baz > bar) {
        console.log( baz );
    }

    // ...
}

這段代碼可以相當容易地重構為:

var foo = true, baz = 10;

if (foo) {
    var bar = 3;

    // ...
}

if (baz > bar) {
    console.log( baz );
}

但是,當使用塊兒作用域變量時要小心這樣的變化:

var foo = true, baz = 10;

if (foo) {
    let bar = 3;

    if (baz > bar) { // <-- 移動時不要忘了`bar`
        console.log( baz );
    }
}

附錄B介紹了一種塊作用域的(更加明確的)替代形式,它可能會在這些場景下提供更易于維護/重構的更健壯的代碼。

const

除了let之外,ES6還引入了const,它也創建一個塊兒作用域變量,但是它的值是固定的(常量)。任何稍后改變它的企圖都將導致錯誤。

var foo = true;

if (foo) {
    var a = 2;
    const b = 3; // 存在于包含它的`if`作用域中

    a = 3; // 沒問題!
    b = 4; // 錯誤!
}

console.log( a ); // 3
console.log( b ); // ReferenceError!

復習

在JavaScript中函數是最常見的作用域單位。在另一個函數內部聲明的變量和函數,實質上對任何外圍“作用域”都是“隱藏的”,這是優秀軟件的一個有意的設計原則。

但是函數絕不是唯一的作用域單位。塊兒作用域指的是這樣一種想法:變量和函數可以屬于任意代碼塊兒(一般來說,就是任意的{ .. }),而不是僅屬于外圍的函數。

從ES3開始,try/catch結構在catch子句上擁有塊兒作用域。

在ES6中,引入了let關鍵字(var關鍵字的表兄弟)允許在任意代碼塊中聲明變量。if (..) { let a = 2; }將會聲明變量a,而它實質上劫持了if{ .. }塊兒的作用域,并將自己附著在這里。

雖然有些人對此深信不疑,但是塊兒作用域不應當被認為是var函數作用域的一個徹頭徹尾的替代品。兩種機能是共存的,而且開發者們可以并且應當同時使用函數作用域和塊兒作用域技術 —— 在它們各自可以產生更好,更易讀/易維護代碼的地方。


  1. Principle of Least Privilege ? ?

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

推薦閱讀更多精彩內容