深入了解JavaScript執(zhí)行過程(JS系列之一)

前言

JavaScript 執(zhí)行過程分為兩個(gè)階段,編譯階段和執(zhí)行階段。在編譯階段 JS 引擎主要做了三件事:詞法分析、語法分析和代碼生成;編譯完成后 JS 引擎開始創(chuàng)建執(zhí)行上下文(JavaScript 代碼運(yùn)行的環(huán)境),并執(zhí)行 JS 代碼。

編譯階段

對(duì)于常見編譯型語言(例如:Java )來說,編譯步驟分為:詞法分析 -> 語法分析 -> 語義檢查 -> 代碼優(yōu)化和字節(jié)碼生成

對(duì)于解釋型語言(例如:JavaScript )來說,編譯階通過詞法分析 -> 語法分析 -> 代碼生成,就可以解釋并執(zhí)行代碼了。

詞法分析

JS 引擎會(huì)將我們寫的代碼當(dāng)成字符串分解成詞法單元(token)。例如,var a = 2 ,這段程序會(huì)被分解成:“var、a、=、2、;” 五個(gè) token 。每個(gè)詞法單元token不可再分割。可以試試這個(gè)網(wǎng)站地址查看 tokenhttps://esprima.org/demo/parse.html

1詞法分析1.png
1詞法分析2.png

語法分析

語法分析階段會(huì)將詞法單元流(數(shù)組),也就是上面所說的token, 轉(zhuǎn)換成樹狀結(jié)構(gòu)的 “抽象語法樹(AST)”

2語法分析.png

代碼生成

AST轉(zhuǎn)換為可執(zhí)行代碼的過程稱為代碼生成,因?yàn)橛?jì)算機(jī)只能識(shí)別機(jī)器指令,需要通過某種方法將 var a = 2; 的 AST 轉(zhuǎn)化為一組機(jī)器指令,用來創(chuàng)建 a 的變量(包括分配內(nèi)存),并將值存儲(chǔ)在 a 中。

執(zhí)行階段

執(zhí)行程序需要有執(zhí)行環(huán)境, Java 需要 Java 虛擬機(jī),同樣解析 JavaScript 也需要執(zhí)行環(huán)境,我們稱它為“執(zhí)行上下文”。

什么是執(zhí)行上下文

簡(jiǎn)而言之,執(zhí)行上下文是對(duì) JavaScript 代碼執(zhí)行環(huán)境的一種抽象,每當(dāng) JavaScript 運(yùn)行時(shí),它都是在執(zhí)行上下文中運(yùn)行。

執(zhí)行上下文類型

JavaScript 執(zhí)行上下文有三種:

  • 全局執(zhí)行上下文 —— 當(dāng) JS 引擎執(zhí)行全局代碼的時(shí)候,會(huì)編譯全局代碼并創(chuàng)建執(zhí)行上下文,它會(huì)做兩件事:1、創(chuàng)建一個(gè)全局的 window 對(duì)象(瀏覽器環(huán)境下),2、將 this 的值設(shè)置為該全局對(duì)象;全局上下文在整個(gè)頁面生命周期有效,并且只有一份。

  • 函數(shù)執(zhí)行上下文 —— 當(dāng)調(diào)用一個(gè)函數(shù)的時(shí)候,函數(shù)體內(nèi)的代碼會(huì)被編譯,并創(chuàng)建函數(shù)執(zhí)行上下文,一般情況下,函數(shù)執(zhí)行結(jié)束之后,創(chuàng)建的函數(shù)執(zhí)行上下文會(huì)被銷毀。

  • eval 執(zhí)行上下文 —— 調(diào)用 eval 函數(shù)也會(huì)創(chuàng)建自己的執(zhí)行上下文(eval函數(shù)容易導(dǎo)致惡意攻擊,并且運(yùn)行代碼的速度比相應(yīng)的替代方法慢,因此不推薦使用)

執(zhí)行棧

執(zhí)行棧這個(gè)概念是比較貼近我們程序員的,學(xué)習(xí)它能讓我們理解 JS 引擎背后工作的原理,開發(fā)中幫助我們調(diào)試代碼,同時(shí)也能應(yīng)對(duì)面試中有關(guān)執(zhí)行棧的面試題。

執(zhí)行棧,在其它編程語言中被叫做“調(diào)用棧”,是一種 LIFO(后進(jìn)先出)棧的數(shù)據(jù)結(jié)構(gòu),被用來存儲(chǔ)代碼運(yùn)行時(shí)創(chuàng)建的所有執(zhí)行上下文。

當(dāng) JS 引擎開始執(zhí)行第一行 JavaScript 代碼時(shí),它會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文然后將它壓到執(zhí)行棧中,每當(dāng)引擎遇到一個(gè)函數(shù)調(diào)用,它會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文并壓入棧的頂部。

引擎會(huì)執(zhí)行那些執(zhí)行上下文位于棧頂?shù)暮瘮?shù)。當(dāng)該函數(shù)執(zhí)行結(jié)束時(shí),執(zhí)行上下文從棧中彈出,控制流程到達(dá)當(dāng)前棧中的下一個(gè)上下文。

結(jié)合下面代碼來理解:

let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
stack.png

當(dāng)上述代碼在瀏覽器加載時(shí),JS 引擎創(chuàng)建了一個(gè)全局執(zhí)行上下文并把它壓入當(dāng)前執(zhí)行棧。當(dāng)遇到 first() JS 引擎為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文并把它壓入當(dāng)前執(zhí)行棧的頂部。

當(dāng)從 first() 函數(shù)內(nèi)部調(diào)用 second() JS 引擎為 second() 函數(shù)創(chuàng)建了一個(gè)新的執(zhí)行上下文并把它壓入當(dāng)前執(zhí)行棧的頂部。當(dāng) second() 函數(shù)執(zhí)行完畢,它的執(zhí)行上下文會(huì)從當(dāng)前棧彈出,并且控制流程到達(dá)下一個(gè)執(zhí)行上下文,即 first() 函數(shù)的執(zhí)行上下文。

當(dāng) first() 執(zhí)行完畢,它的執(zhí)行上下文從棧彈出,控制流程到達(dá)全局執(zhí)行上下文。一旦所有代碼執(zhí)行完畢,JavaScript 引擎從當(dāng)前棧中移除全局執(zhí)行上下文。

如何創(chuàng)建執(zhí)行上下文

現(xiàn)在我們已經(jīng)了解了 JS 引擎是如何去管理執(zhí)行上下文的,那么,執(zhí)行上下文是如何創(chuàng)建的呢?

執(zhí)行上下文的創(chuàng)建分為兩個(gè)階段:

  • 創(chuàng)建階段;
  • 執(zhí)行階段;

創(chuàng)建階段

執(zhí)行上下文創(chuàng)建階段會(huì)做三件事:

  • 綁定 this
  • 創(chuàng)建詞法環(huán)境
  • 創(chuàng)建變量環(huán)境

所以執(zhí)行上下文在概念上表示如下:

ExecutionContext = { // 執(zhí)行上下文
  Binding This, // this值綁定
  LexicalEnvironment = { ... }, // 詞法環(huán)境
  VariableEnvironment = { ... }, // 變量環(huán)境
}
綁定 this

在全局執(zhí)行上下文中,this 的值指向全局對(duì)象。(在瀏覽器中,this 引用 Window 對(duì)象)。

在函數(shù)執(zhí)行上下文中,this 的值取決于該函數(shù)是如何被調(diào)用的

  • 通過對(duì)象方法調(diào)用函數(shù),this 指向調(diào)用的對(duì)象
  • 聲明函數(shù)后使用函數(shù)名稱普通調(diào)用,this 指向全局對(duì)象,嚴(yán)格模式下 this 值是 undefined
  • 使用 new 方式調(diào)用函數(shù),this 指向新創(chuàng)建的對(duì)象
  • 使用 callapplybind 方式調(diào)用函數(shù),會(huì)改變 this 的值,指向傳入的第一個(gè)參數(shù),例如

function fn () {
  console.log(this)
}

function fn1 () {
  'use strict'
  console.log(this)
}

fn() // 普通函數(shù)調(diào)用,this 指向window對(duì)象
fn() // 嚴(yán)格模式下,this 值為 undefined

let foo = {
  baz: function() {
  console.log(this);
  }
}

foo.baz();   // 'this' 指向 'foo'

let bar = foo.baz;

bar();       // 'this' 指向全局 window 對(duì)象,因?yàn)闆]有指定引用對(duì)象

let obj {
  name: 'hello'
}

foo.baz.call(obj) // call 改變this值,指向obj對(duì)象
詞法環(huán)境

每一個(gè)詞法環(huán)境由下面兩部分組成:

  • 環(huán)境記錄:變量對(duì)象 =》存儲(chǔ)聲明的變量和函數(shù)( let, const, function,函數(shù)參數(shù))
  • 外部環(huán)境引用:作用域鏈

ES6的官方文檔 把詞法環(huán)境定義為:

詞法環(huán)境(Lexical Environments)是一種規(guī)范類型,用于根據(jù)ECMAScript代碼的詞法嵌套結(jié)構(gòu)來定義標(biāo)識(shí)符與特定變量和函數(shù)的關(guān)聯(lián)。詞法環(huán)境由一個(gè)環(huán)境記錄(Environment Record)和一個(gè)可能為空的外部詞法環(huán)境(outer Lexical Environment)引用組成。

簡(jiǎn)單來說,詞法環(huán)境就是一種標(biāo)識(shí)符—變量映射的結(jié)構(gòu)(這里的標(biāo)識(shí)符指的是變量/函數(shù)的名字,變量是對(duì)實(shí)際對(duì)象[包含函數(shù)和數(shù)組類型的對(duì)象]或基礎(chǔ)數(shù)據(jù)類型的引用)。

舉個(gè)例子,看看下面的代碼:

var a = 20;
var b = 40;
function foo() {
  console.log('bar');
}

上面代碼的詞法環(huán)境類似這樣:

lexicalEnvironment = {
  a: 20,
  b: 40,
  foo: <ref. to foo function>
}

環(huán)境記錄

所謂的環(huán)境記錄就是詞法環(huán)境中記錄變量和函數(shù)聲明的地方

環(huán)境記錄也有兩種類型:

聲明類環(huán)境記錄。顧名思義,它存儲(chǔ)的是變量和函數(shù)聲明,函數(shù)的詞法環(huán)境內(nèi)部就包含著一個(gè)聲明類環(huán)境記錄。

對(duì)象環(huán)境記錄。全局環(huán)境中的詞法環(huán)境中就包含的就是一個(gè)對(duì)象環(huán)境記錄。除了變量和函數(shù)聲明外,對(duì)象環(huán)境記錄還包括全局對(duì)象(瀏覽器的window對(duì)象)。因此,對(duì)于對(duì)象的每一個(gè)新增屬性(對(duì)瀏覽器來說,它包含瀏覽器提供給window對(duì)象的所有屬性和方法),都會(huì)在該記錄中創(chuàng)建一個(gè)新條目。

注意:對(duì)函數(shù)而言,環(huán)境記錄還包含一個(gè)arguments對(duì)象,該對(duì)象是個(gè)類數(shù)組對(duì)象,包含參數(shù)索引和參數(shù)的映射以及一個(gè)傳入函數(shù)的參數(shù)的長(zhǎng)度屬性。舉個(gè)例子,一個(gè)arguments對(duì)象像下面這樣:

function foo(a, b) {
  var c = a + b;
}
foo(2, 3);
// argument 對(duì)象類似下面這樣
Arguments: { 0: 2, 1: 3, length: 2 }

環(huán)境記錄對(duì)象在創(chuàng)建階段也被稱為變量對(duì)象(VO),在執(zhí)行階段被稱為活動(dòng)對(duì)象(AO)。之所以被稱為變量對(duì)象是因?yàn)榇藭r(shí)該對(duì)象只是存儲(chǔ)執(zhí)行上下文中變量和函數(shù)聲明,之后代碼開始執(zhí)行,變量會(huì)逐漸被初始化或是修改,然后這個(gè)對(duì)象就被稱為活動(dòng)對(duì)象

外部環(huán)境引用

對(duì)于外部環(huán)境的引用意味著在當(dāng)前執(zhí)行上下文中可以訪問外部詞法環(huán)境。也就是說,如果在當(dāng)前的詞法環(huán)境中找不到某個(gè)變量,那么Javascript引擎會(huì)試圖在上層的詞法環(huán)境中尋找。(Javascript引擎會(huì)根據(jù)這個(gè)屬性來構(gòu)成我們常說的作用域鏈)

詞法環(huán)境抽象出來類似下面的偽代碼:

GlobalExectionContext = { // 全局執(zhí)行上下文
  this: <global object> // this 值綁定
  LexicalEnvironment: { // 全局執(zhí)行上下文詞法環(huán)境
    EnvironmentRecord: {  // 環(huán)境記錄
      Type: "Object",
        // 標(biāo)識(shí)符在這里綁定
    }
    outer: <null> // 外部引用
  }
}
FunctionExectionContext = { // 函數(shù)執(zhí)行上下文
  this: <depends on how function is called> // this 值綁定
  LexicalEnvironment: { // 函數(shù)執(zhí)行上下文詞法環(huán)境
    EnvironmentRecord: { // 環(huán)境記錄
      Type: "Declarative",
      // 標(biāo)識(shí)符在這里綁定
    }
    outer: <Global or outer function environment reference> // 引用全局環(huán)境
   }
}
變量環(huán)境

它同樣是一個(gè)詞法環(huán)境,其環(huán)境記錄器持有變量聲明語句在執(zhí)行上下文中創(chuàng)建的綁定關(guān)系。

如上所述,變量環(huán)境也是一個(gè)詞法環(huán)境,所以它有著上面定義的詞法環(huán)境的所有屬性。

在 ES6 中,詞法環(huán)境變量環(huán)境的一個(gè)不同就是前者被用來存儲(chǔ)函數(shù)聲明和變量(let 和 const)綁定,而后者只用來存儲(chǔ) var 變量綁定。

看點(diǎn)樣例代碼來理解上面的概念:

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
 var g = 20;
 return e * f * g;
}

c = multiply(20, 30);

執(zhí)行起來看起來像這樣:

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在這里綁定標(biāo)識(shí)符
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在這里綁定標(biāo)識(shí)符
      c: undefined,
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  ThisBinding: <Global Object>,
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在這里綁定標(biāo)識(shí)符
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>
  },

VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在這里綁定標(biāo)識(shí)符
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}

注意 — 只有遇到調(diào)用函數(shù) multiply 時(shí),函數(shù)執(zhí)行上下文才會(huì)被創(chuàng)建。

可能你已經(jīng)注意到 letconst 定義的變量并沒有關(guān)聯(lián)任何值,但 var 定義的變量被設(shè)成了 undefined

這是因?yàn)樵趧?chuàng)建階段時(shí),引擎檢查代碼找出變量和函數(shù)聲明,雖然函數(shù)聲明完全存儲(chǔ)在環(huán)境中,但是變量最初設(shè)置為 undefinedvar 情況下),或者未初始化(letconst 情況下)。

這就是為什么你可以在聲明之前訪問 var 定義的變量(雖然是 undefined),但是在聲明之前訪問 letconst 的變量會(huì)得到一個(gè)引用錯(cuò)誤。

這就是我們說的變量聲明提升。

執(zhí)行階段

經(jīng)過上面的創(chuàng)建執(zhí)行上下文,就開始執(zhí)行 JavaScript 代碼了。在執(zhí)行階段,如果 JavaScript 引擎不能在源碼中聲明的實(shí)際位置找到 let 變量的值,它會(huì)被賦值為 undefined

執(zhí)行棧應(yīng)用

利用瀏覽器查看棧的調(diào)用信息

我們知道執(zhí)行棧是用來管理執(zhí)行上下文調(diào)用關(guān)系的數(shù)據(jù)結(jié)構(gòu),那么我們?cè)趯?shí)際工作中如何運(yùn)用它呢。

答案是我們可以借助瀏覽器“開發(fā)者工具” source 標(biāo)簽,選擇 JavaScript 代碼打上斷點(diǎn),就可以查看函數(shù)的調(diào)用關(guān)系,并且可以切換查看每個(gè)函數(shù)的變量值

調(diào)用棧.png

我們?cè)?second 函數(shù)內(nèi)部打上斷點(diǎn),就可以看到右邊 Call Stack 調(diào)用棧顯示 secondfirst(anonymous) 調(diào)用關(guān)系,second 是在棧頂(anonymous 在棧底相當(dāng)于全局執(zhí)行上下文),執(zhí)行second函數(shù)我們可以查看該函數(shù)作用域 Scope 局部變量abnum的值,通過查看調(diào)用棧的調(diào)用關(guān)系我們可以快速定位到我們代碼執(zhí)行的情況。

那如果代碼執(zhí)行出錯(cuò),也不知道在哪個(gè)地方打斷點(diǎn)調(diào)試,那怎么查看出錯(cuò)地方的調(diào)用棧呢,告訴大家一個(gè)技巧,如下圖

調(diào)用棧2.png

我們不用打斷點(diǎn),執(zhí)行上面兩步操作,就可以在代碼執(zhí)行異常的地方自動(dòng)打上斷點(diǎn)。知道這個(gè)技巧后,再也不用擔(dān)心代碼出錯(cuò)了。

除了上面通過斷點(diǎn)來查看調(diào)用棧,還可以使用 console.trace() 來輸出當(dāng)前的函數(shù)調(diào)用關(guān)系,比如在示例代碼中的 second 函數(shù)里面加上了 console.trace(),就可以看到控制臺(tái)輸出的結(jié)果,如下圖:

調(diào)用棧3.png

總結(jié)

JavaScript執(zhí)行分為兩個(gè)階段,編譯階段和執(zhí)行階段。編譯階段會(huì)經(jīng)過詞法分析、語法分析、代碼生成步驟生成可執(zhí)行代碼; JS 引擎執(zhí)行可執(zhí)行性代碼會(huì)創(chuàng)建執(zhí)行上下文,包括綁定this、創(chuàng)建詞法環(huán)境和變量環(huán)境;詞法環(huán)境創(chuàng)建外部引用(作用域鏈)和 記錄環(huán)境(變量對(duì)象,let, const, function, arguments), JS 引擎創(chuàng)建執(zhí)行上下完成后開始單線程從上到下一行一行執(zhí)行 JS 代碼了。

最后,分享了在開發(fā)過程中一些調(diào)用棧的的應(yīng)用技巧。

引用鏈接

JavaScript 語法解析、AST、V8、JIT

[譯] 理解 JavaScript 中的執(zhí)行上下文和執(zhí)行棧

理解Javascript中的執(zhí)行上下文和執(zhí)行棧

推薦閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,701評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,694評(píng)論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,026評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評(píng)論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,193評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,719評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,668評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,846評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評(píng)論 1 295
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,394評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,635評(píng)論 2 380

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