javascript高級程序設計(第4章)-- 變量、作用域和內存問題

第四章:變量、作用域和內存問題

本章內容:

  • 理解基本類型和引用類型
  • 理解執行環境
  • 理解垃圾回收機制

4.1 基本類型和引用類型

ECMAScript中的變量包含兩種不同類型的值: 基本類型值引用類型值

基本類型有:Undefined、Null、Boolean、Number、String。

這五種數據類型是按值訪問的。

引用類型的值是保存在內存中的對象。 javascript不允許直接訪問內存位置。

引用類型的值按引用訪問的。

4.1.1 動態屬性

// 創建一個引用類型
var person = new Object();
person.name = 'zhangzhuo';
alert(person.name);  //zhangzhuo

// 創建一個基本類型
var name = 'zhangzhuo';
alert(name.toUpperCase()); //ZHANGZHUO
name.age = 18;
alert(name.age);  //error

這里雖然name能調用String.toUpperCase是因為基本類型自動創建了基本包裝類型String的實例。但在該行運行后便清空了。

基本類型不能添加屬性。

不可變的基本類型與可變的引用類型

4.1.2 復制變量值

從一個變量從另外一個變量復制基本類型值和引用類型時,也存在不同。

復制基本類型:
var num1 = 20;
var num2 = num1;
num2 = 30;
復制基本類型

在變量對象中的數據發生復制行為時,系統會自動為新的變量分配一個新值。var num2 = num1執行之后,num1與num2雖然值都等于20,但是他們其實已經是相互獨立互不影響的值了。具體如圖。所以我們修改了num2的值以后,num1的值并不會發生變化。

復制引用類型:
var obj1 = {a:10,b:15};
var obj2 = obj1;
obj1.a = 20;
alert(obj2.a); // 20

我們通過var obj1 = obj2執行一次復制引用類型的操作。引用類型的復制同樣也會為新的變量自動分配一個新的值保存在變量對象中,但不同的是,這個新的值,僅僅只是引用類型的一個地址指針。當地址指針相同時,盡管他們相互獨立,但是在變量對象中訪問到的具體對象實際上是同一個。如圖所示。

因此當我改變obj1時,obj2也發生了變化。這就是引用類型的特性。

復制引用類型

4.1.3 傳遞參數

ECMAScript中所有函數的參數均是按值傳遞。也就是說,會把函數外部的值復制給函數內部的參數,就把值從一個變量復制給另一個變量相同。

在向參數傳遞引用類型的時候,其實會把這個值在內存的地址復制給局部變量。

// demo1 傳遞基本類型
function addTen(num){
    num += 10;
    return num;
}
var count = 20;
var result = addTen(count);
alert(count); // 20
alert(result); // 30

從demo1可知道,傳遞的count變量,數字20被復制給了變量num。num的數值增加了10,并不會影響外層的count。

// demo2 傳遞引用類型
function setName(obj){
    obj.name = 'zhangzhuo';
}
var person = new Object();
setName(person);
alert(person.name); // zhangzhuo

person變量的內存值復制給了obj。obj和person指向同一個對象,所以當函數內部改變obj的屬性的時候,person也會發生了變化。

證明:對象是傳值而不是傳引用
//demo3 證明對象是傳值
function setName(obj){
    obj.name = 'zhangzhuo';
    obj = new Object();
    obj.name = 'dudu';
}
var person = new Object();
setName(person);
alert(person.name); // zhangzhuo

如果person是傳遞引用,那么person就會指向name為'dudu'的新對象。但是,訪問person.name的時候顯示仍然是zhangzhuo。說明對象是傳值而非傳遞引用。

4.1.4 檢測類型

檢測一個基本類型可以用typeof

檢測引用類型(判斷變量是什么類型的對象),ECMAScript提供了instanceof(原理:根據原型鏈來識別)。用法:

result = variable instanceof constructor;  // 返回值 true or false
// eg:
alert(person instanceof Object); //變量person是Object嗎
alert(colors instanceof Array); //變量colors是Array嗎

如果使用instanceof檢測基本類型,會返回false。因為基本類型不是對象。

延伸閱讀1: 理解內存分配

堆與棧

是一種FIFO(Last-In-First-Out)后進先出的數據結構,在javascript中我們可以用Array模擬。

var arr = []; // 創建一個棧
array.push('apple'); // 壓入一個元素apple ['apple']
array.push('orange'); // 壓入一個元素orange ['apple','orange']
array.pop(); // 彈出orange ['apple']
array.push('banana'); // 壓入一個元素banana ['apple','banana']

基本類型值是存儲在棧中的簡單數據段,也就是說,他們的值直接存儲在變量訪問的位置。

是存放數據的一種離散數據結構,在javascript中,引用值是存放在堆中的。

那為什么引用值要放在堆中,而原始值要放在棧中,不都是在內存中嗎,為什么不放在一起呢?那接下來,讓我們來探索問題的答案!

function Person(id,name,age){
    this.id = id;
    this.name = name;
    this.age = age;
}
 
var num = 10;
var bol = true;
var str = "abc";
var obj = new Object();
var arr = ['a','b','c'];
var person = new Person(100,"zhangzhuo",25);

然后我們來看一下內存分析圖:

堆與棧分析

變量num,bol,str為基本數據類型,它們的值,直接存放在棧中,obj,person,arr為復合數據類型,他們的引用變量存儲在棧中,指向于存儲在堆中的實際對象。

由上圖可知,我們無法直接操縱堆中的數據,也就是說我們無法直接操縱對象,但我們可以通過棧中對對象的引用來操作對象,就像我們通過遙控機操作電視機一樣,區別在于這個電視機本身并沒有控制按鈕。

現在讓我們來回答為什么引用值要放在堆中,而原始值要放在棧中的問題:

記住一句話:能量是守衡的,無非是時間換空間,空間換時間的問題

堆比棧大,棧比堆的運算速度快,對象是一個復雜的結構,并且可以自由擴展,如:數組可以無限擴充,對象可以自由添加屬性。將他們放在堆中是為了不影響棧的效率。而是通過引用的方式查找到堆中的實際對象再進行操作。相對于簡單數據類型而言,簡單數據類型就比較穩定,并且它只占據很小的內存。不將簡單數據類型放在堆是因為通過引用到堆中查找實際對象是要花費時間的,而這個綜合成本遠大于直接從棧中取得實際值的成本。所以簡單數據類型的值直接存放在棧中。

4.2 執行環境和作用域

執行函數

執行環境(execution context, 有的地方也翻譯為執行上下文)是javascript中最重要的一個概念。執行環境定義了變量或者函數有權訪問的其他數據。每個執行環境都有一個與之關聯的變量對象(variable object),環境中所有定義的變量和函數都保存在這個對象中。

全局執行環境是最外圍的一個執行環境。

每個函數都有自己的執行環境,當執行流進入一個函數的時候,函數的環境就會被推入一個環境棧中,而這個函數執行完畢后,棧將其環境彈出,把控制權返回之前的執行環境。ECMAScript程序中的執行流就是由這個方便的機制控制著。

延伸閱讀2: 理解執行環境

每次當控制器轉到可執行代碼的時候,就會進入當前代碼的執行環境,它會形成一個作用域。JavaScript中的運行環境大概包括三種情況。

  • 全局環境:JavaScript代碼運行起來會首先進入該環境;
  • 函數環境:當函數被調用執行時,會進入當前函數中執行代碼 ;
  • evel: (不建議使用,忽略);

因此在一個JavaScript程序中,必定會產生多個執行環境,在我的上一篇文章中也有提到,JavaScript引擎會以棧的方式來處理它們,這個棧,我們稱其為函數調用棧(call stack)。棧底永遠都是全局環境,而棧頂就是當前正在執行的環境。

當代碼在執行過程中,遇到以上三種情況,都會生成一個執行環境,放入棧中,而處于棧頂的環境執行完畢之后,就會自動出棧。為了更加清晰的理解這個過程,根據下面的例子,結合圖示給大家展示。

執行上下文可以理解為函數執行的環境,每一個函數執行時,都會給對應的函數創建這樣一個執行環境。

var color = 'blue';

function changeColor() {
    var anotherColor = 'red';

    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }

    swapColors();
}

changeColor();

我們用ECStack來表示處理執行環境的的堆棧。我們很容易知道,第一步,首先是全局環境入棧。

全局環境入棧

全局環境入棧之后,其中的可執行代碼開始執行,直到遇到了changeColor(),這一句激活函數changeColor創建它自己的執行環境,因此第二步就是changeColor的執行環境入棧。

changeColor入棧

changeColor的環境入棧之后,控制器開始執行其中的可執行代碼,遇到swapColors()之后又激活了一個執行環境。因此第三步是swapColors的執行上下文入棧。

swapColor入棧

在swapColors的可執行代碼中,再沒有遇到其他能生成執行環境的情況,因此這段代碼順利執行完畢,swapColors的環境從棧中彈出。

swapColor出棧

swapColors的執行環境彈出之后,繼續執行changeColor的可執行代碼,也沒有再遇到其他執行環境,順利執行完畢之后彈出。這樣,ECStack中就只身下全局環境了。

changeColor出棧

全局上下文在瀏覽器窗口關閉后出棧。

圖解函數調用棧

詳細了解了這個過程之后,我們就可以對執行上下文總結一些結論了。

  • js是單線程的;
  • 同步執行,只有棧頂的環境處于執行中,其他上下文需要等待
  • 全局環境只有唯一的一個,它在瀏覽器關閉時出棧
  • 函數的執行環境的個數沒有限制
  • 每次某個函數被調用,就會有個新的執行環境為其創建,即使是調用的自身函數,也是如此。

為了鞏固一下執行環境的理解,我們再來繪制一個例子的演變過程,這是一個簡單的閉包例子。

function f1(){
    var n=999;
    function f2(){
        alert(n);
    }
    return f2;
}
var result=f1();
result(); // 999

因為f1中的函數f2在f1的可執行代碼中,并沒有被調用執行,因此執行f1時,f2不會創建新的上下文,而直到result執行時,才創建了一個新的。具體演變過程如下。 (入棧相當于要執行代碼)

mark

作用域和作用域鏈

作用域:
  • 在JavaScript中,我們可以將作用域定義為一套規則,這套規則用來管理引擎如何在當前作用域以及嵌套的子作用域中根據標識符名稱進行變量查找。
  • 作用域與執行環境是完全不同的兩個概念。我知道很多人會混淆他們,但是一定要仔細區分。
  • JavaScript中只有全局作用域與函數作用域(因為eval我們平時開發中幾乎不會用到它,這里不討論)。

JavaScript代碼的整個執行過程,分為兩個階段,代碼編譯階段與代碼執行階段。編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段作用域規則會確定。執行階段由引擎完成,主要任務是執行可執行代碼,執行上下文在這個階段創建。

mark
作用域鏈:

作用域鏈,是由當前環境與上層環境的一系列變量對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。

當代碼在一個環境中執行的時候,會創建變量對象和一個作用域鏈(scope chain)。是保證對執行環境有權訪問所有變量和函數的有序訪問。作用域鏈的前端,始終是當前的執行環境的變量對象。如果這個環境是函數,則將其變量對象(activation object)作為活動對象。變量對象最開始只包含一個變量,即arguments對象(這個對象在全局環境中是不存在的)。

標識符的解析是沿著作用域鏈一級一級地搜索標識符的過程。搜索過程始終是從作用域鏈的前端開始。

var color = 'blue';

function changeColor(){
    if(color === 'blue'){
        color = 'red';
    } else {
        color = 'blue';
    }
}

changeColor();
alert(color); //red

在這個例子中,函數changeColor的作用域鏈包含兩個對象,它自己的變量對象arguments和全局環境的變量對象。可以在函數內部訪問變量color,就是因為可以在作用域鏈找到它。

延伸閱讀3: 作用域與作用域鏈

在訪問一個變量的時候,就必須存在一個可見性的問題,這就是作用域。更深入的說,當訪問一個變量或者調用一個函數的時候,javaScript引擎將不同執行位置上的變量對象按照規則構建一個鏈表。在訪問一個變量的時候,先從鏈表的第一個變量對象中查找,如果沒有則在第二個變量對象中查找,直到搜索結束。這也就形成了作用域鏈的概念。

延伸閱讀4: 變量對象詳解

當調用一個函數時(激活),一個新的執行環境就會被創建。而一個執行環境的生命周期可以分為兩個階段。

  • 創建階段

在這個階段中,執行上下文會分別創建變量對象,建立作用域鏈,以及確定this的指向。

  • 代碼執行階段

創建完成之后,就會開始執行代碼,這個時候,會完成變量賦值,函數引用,以及執行其他代碼。

mark

變量對象(Variable Object)

變量對象的創建,依次經歷了以下幾個過程。

  1. 建立arguments對象。檢查當前執行環境中的參數,建立該對象下的屬性與屬性值。
  2. 檢查當前執行環境的函數聲明,也就是使用function關鍵字聲明的函數。在變量對象中以函數名建立一個屬性,屬性值為指向該函數所在內存地址的引用。如果函數名的屬性已經存在,那么該屬性將會被新的引用所覆蓋。
  3. 檢查當前執行環境中的變量聲明,每找到一個變量聲明,就在變量對象中以變量名建立一個屬性,屬性值為undefined。如果該變量名的屬性已經存在,為了防止同名的函數被修改為undefined,則會直接跳過,原屬性值不會被修改。

許多讀者在閱讀到這的時候會因為下面的這樣場景對于“跳過”一詞產生疑問。既然變量聲明的foo遇到函數聲明的foo會跳過,可是為什么最后foo的輸出結果仍然是被覆蓋了?

function foo() { console.log('function foo') }
var foo = 20;

console.log(foo); // 20

其實只是大家在閱讀的時候不夠仔細,因為上面的三條規則僅僅適用于變量對象的創建過程。也就是執行環境的創建過程。而foo = 20是在執行環境的執行過程中運行的,輸出結果自然會是20。對比下例。

console.log(foo); // function foo
function foo() { console.log('function foo') }
var foo = 20;
// 上例的執行順序為

// 首先將所有函數聲明放入變量對象中
function foo() { console.log('function foo') }

// 其次將所有變量聲明放入變量對象中,但是因為foo已經存在同名函數,因此此時會跳過undefined的賦值
// var foo = undefined;

// 然后開始執行階段代碼的執行
console.log(foo); // function foo
foo = 20;
mark

根據這個規則,理解變量提升就變得十分簡單了。

在上面的規則中我們看出,function聲明會比var聲明優先級更高一點。為了幫助大家更好的理解變量對象,我們結合一些簡單的例子來進行探討。

// demo01
function test() {
    console.log(a);
    console.log(foo());

    var a = 1;
    function foo() {
        return 2;
    }
}

test();

在上例中,我們直接從test()的執行環境開始理解。全局作用域中運行test()時,test()的執行上下文開始創建。為了便于理解,我們用如下的形式來表示

// 創建過程
testEC = {
    // 變量對象
    VO: {},
    scopeChain: {}
}

// 因為本文暫時不詳細解釋作用域鏈,所以把變量對象專門提出來說明

// VO 為 Variable Object的縮寫,即變量對象
VO = {
    arguments: {...},  //注:在瀏覽器的展示中,函數的參數可能并不是放在arguments對象中,這里為了方便理解,我做了這樣的處理
    foo: <foo reference>  // 表示foo的地址引用
    a: undefined
}

未進入執行階段之前,變量對象中的屬性都不能訪問!但是進入執行階段之后,變量對象轉變為了活動對象,里面的屬性都能被訪問了,然后開始進行執行階段的操作。

這樣,如果再面試的時候被問到變量對象和活動對象有什么區別,就又可以自如的應答了,他們其實都是同一個對象,只是處于執行環境的不同生命周期。不過只有處于函數調用棧棧頂的執行環境中的變量對象,才會變成活動對象。

// 執行階段
VO ->  AO   // Active Object
AO = {
    arguments: {...},
    foo: <foo reference>,
    a: 1,
    this: Window
}

因此,上面的例子demo1,執行順序就變成了這樣

function test() {
    function foo() {
        return 2;
    }
    var a;
    console.log(a);
    console.log(foo());
    a = 1;
}

test();

再來一個例子,鞏固一下我們的理解。

// demo2
function test() {
    console.log(foo);
    console.log(bar);

    var foo = 'Hello';
    console.log(foo);
    var bar = function () {
        return 'world';
    }

    function foo() {
        return 'hello';
    }
}

test();
// 創建階段
VO = {
    arguments: {...},
    foo: <foo reference>,
    bar: undefined
}
// 這里有一個需要注意的地方,因為var聲明的變量當遇到同名的屬性時,會跳過而不會覆蓋
// 執行階段
VO -> AO
VO = {
    arguments: {...},
    foo: 'Hello',
    bar: <bar reference>,
    this: Window
}

延伸閱讀5: 詳細圖解作用域鏈與閉包

作用域鏈,是由當前環境與上層環境的一系列變量對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。

為了幫助大家理解作用域鏈,我我們先結合一個例子,以及相應的圖示來說明。

var a = 20;
function test(){
    var b = a + 10;
    
    function innerTest(){
        var c = 10;
        return b + c;
    }
    
    return innerTest();
}

console.log(test());

在上面的例子中,全局,函數test,函數innerTest的執行上下文先后創建。我們設定他們的變量對象分別為VO(global),VO(test), VO(innerTest)。而innerTest的作用域鏈,則同時包含了這三個變量對象,所以innerTest的執行上下文可如下表示。

innerTestEC = {
    VO: {...},  // 變量對象
    scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域鏈
}

我們可以直接用一個數組來表示作用域鏈,數組的第一項scopeChain[0]為作用域鏈的最前端,而數組的最后一項,為作用域鏈的最末端,所有的最末端都為全局變量對象。

很多人會誤解為當前作用域與上層作用域為包含關系,但其實并不是。以最前端為起點,最末端為終點的單方向通道我認為是更加貼切的形容。如圖。

mark

注意,因為變量對象在執行上下文進入執行階段時,就變成了活動對象,這一點在上一篇文章中已經講過,因此圖中使用了AO來表示。Active Object

是的,作用域鏈是由一系列變量對象組成,我們可以在這個單向通道中,查詢變量對象中的標識符,這樣就可以訪問到上一層作用域中的變量了。

小結:

javascript變量可以保存兩種類型的值:基本類型值與引用類型值。基本類型的值源于以下五種基本數據類型:Undefined、Null、Boolean、Number、String。基本類型的值與引用類型的值具有以下的特點:

  • 基本類型值在內存中占據固定大小空間,因此被保存在棧內存中;
  • 從一個變量向另一個變量復制基本類型的值,會創建該值得副本;
  • 引用類型的值是對象,保存在堆內存中;
  • 包含引用類型的變量實際上包含的并不是對象本身,而是指向該對象的指針;
  • 從一個變量向另一個變量復制引用類型的值,復制其實是指針,因此兩個變量最終會指向同一個對象;
  • 確定一個值是哪種基本類型可以用typeof操作符,而確定一個值是哪種引用類型用instanceof操作符;

所有的變量(包括基本類型和引用類型)都存在一個執行環境中,這個執行環境決定了變量的生命周期,以及哪一部分代碼可以訪問其中的變量。以下是關于執行環境的總結:

  • 執行環境有全局執行環境和函數執行環境之分;
  • 每次進入一個新的執行環境,都會創建一個用于搜索變量和函數的作用域鏈,和一個變量對象;
  • 通過作用域鏈,函數中的執行環境不僅能夠訪問函數作用域中的變量,而且有權訪問其父環境,乃至全局執行環境;
  • 變量的執行環境有助于確定應該何時釋放內存;

javascript是一門具有自動垃圾回收機制的編程語言,開發人員不必關心內存分配和回收問題。以下有關回收的總結:

  • 離開作用域的值被自動標記為可以回收,因此將在垃圾收集期間刪除;
  • 標記清除是目前最流行的垃圾回收算法,這種算法的思想是給當前不使用的值加上標記,然后再回收;
  • 另外一種垃圾收集算法是引用計數,這種算法的思想是跟蹤記錄所有值被引用的次數,IE舊版本使用這種算法;
  • 當代碼存在循環引用的時候,引用計數算法就會導致問題;
  • 接觸變量的引用(x = null)不僅有助于消除循環引用現象,對垃圾回收也有好處。為了確保有效的回收內存,應該及時解除不再使用的全局對象、全局對象屬性以及循環變量的引用。

參考:

理解Javascript_01_理解內存分配

理解Javascript_15_作用域分配與變量訪問規則,再送個閉包

前端基礎進階(一):內存空間詳細圖解

前端基礎進階(二):執行上下文詳細圖解

前端基礎進階(三):變量對象詳解

前端基礎進階(四):詳細圖解作用域鏈與閉包

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

推薦閱讀更多精彩內容