第三章:對象

特別說明,為便于查閱,文章轉自https://github.com/getify/You-Dont-Know-JS

在第一和第二章中,我們講解了 this 綁定如何根據(jù)函數(shù)調用的調用點指向不同的對象。但究竟什么是對象,為什么我們需要指向它們?這一章我們就來詳細探索一下對象。

語法

對象來自于兩種形式:聲明(字面)形式,和構造形式。

一個對象的字面語法看起來像這樣:

var myObj = {
    key: value
    // ...
};

構造形式看起來像這樣:

var myObj = new Object();
myObj.key = value;

構造形式和字面形式的結果是完全同種類的對象。唯一真正的區(qū)別在于你可以向字面聲明一次性添加一個或多個鍵/值對,而對于構造形式,你必須一個一個地添加屬性。

注意: 像剛才展示的那樣使用“構造形式”來創(chuàng)建對象是極其少見的。你很有可能總是想使用字面語法形式。這對大多數(shù)內建的對象也一樣(后述)。

類型

對象是大多數(shù) JS 程序依賴的基本構建塊兒。它們是 JS 的六種主要類型(在語言規(guī)范中稱為“語言類型”)中的一種:

  • string
  • number
  • boolean
  • null
  • undefined
  • object

注意 簡單基本類型stringnumber、booleannull、和 undefined)自身 不是 object。null 有時會被當成一個對象類型,但是這種誤解源自于一個語言中的 Bug,它使得 typeof null 錯誤地(而且令人困惑地)返回字符串 "object"。實際上,null 是它自己的基本類型。

一個常見的錯誤論斷是“JavaScript中的一切都是對象”。這明顯是不對的。

對比來看,存在幾種特殊的對象子類型,我們可以稱之為 復雜基本類型

function 是對象的一種子類型(技術上講,叫做“可調用對象”)。函數(shù)在 JS 中被稱為“頭等(first class)”類型,是因為它們基本上就是普通的對象(附帶有可調用的行為語義),而且它們可以像其他普通的對象那樣被處理。

數(shù)組也是一種形式的對象,帶有特別的行為。數(shù)組在內容的組織上要稍稍比一般的對象更加結構化。

內建對象

有幾種其他的對象子類型,通常稱為內建對象。對于其中的一些來說,它們的名稱看起來暗示著它們和它們對應的基本類型有著直接的聯(lián)系,但事實上,它們的關系更復雜,我們一會兒就開始探索。

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

如果你依照和其他語言的相似性來看的話,比如 Java 語言的 String 類,這些內建類型有著實際類型的外觀,甚至是類(class)的外觀,

但是在 JS 中,它們實際上僅僅是內建的函數(shù)。這些內建函數(shù)的每一個都可以被用作構造器(也就是一個可以通過 new 操作符調用的函數(shù) —— 參照第二章),其結果是一個新 構建 的相應子類型的對象。例如:

var strPrimitive = "I am a string";
typeof strPrimitive;                            // "string"
strPrimitive instanceof String;                 // false

var strObject = new String( "I am a string" );
typeof strObject;                               // "object"
strObject instanceof String;                    // true

// 考察 object 子類型
Object.prototype.toString.call( strObject );    // [object String]

我們會在本章稍后詳細地看到 Object.prototype.toString... 到底是如何工作的,但簡單地說,我們可以通過借用基本的默認 toString() 方法來考察內部子類型,而且你可以看到它揭示了 strObject 實際上是一個由 String 構造器創(chuàng)建的對象。

基本類型值 "I am a string" 不是一個對象,它是一個不可變的基本字面值。為了對它進行操作,比如檢查它的長度,訪問它的各個獨立字符內容等等,都需要一個 String 對象。

幸運的是,在必要的時候語言會自動地將 "string" 基本類型強制轉換為 String 對象類型,這意味著你幾乎從不需要明確地創(chuàng)建對象。JS 社區(qū)的絕大部分人都 強烈推薦 盡可能地使用字面形式的值,而非使用構造的對象形式。

考慮下面的代碼:

var strPrimitive = "I am a string";

console.log( strPrimitive.length );         // 13

console.log( strPrimitive.charAt( 3 ) );    // "m"

在這兩個例子中,我們在字符串的基本類型上調用屬性和方法,引擎會自動地將它強制轉換為 String 對象,所以這些屬性/方法的訪問可以工作。

當使用如 42.359.toFixed(2) 這樣的方法時,同樣的強制轉換也發(fā)生在數(shù)字基本字面量 42 和包裝對象 new Nubmer(42) 之間。同樣的還有 Boolean 對象和 "boolean" 基本類型。

nullundefined 沒有對象包裝的形式,僅有它們的基本類型值。相比之下,Date 的值 僅可以 由它們的構造對象形式創(chuàng)建,因為它們沒有對應的字面形式。

無論使用字面還是構造形式,Object、Array、Function、和 RegExp(正則表達式)都是對象。在某些情況下,構造形式確實會比對應的字面形式提供更多的創(chuàng)建選項。因為對象可以被任意一種方式創(chuàng)建,更簡單的字面形式幾乎是所有人的首選。僅僅在你需要使用額外的選項時使用構建形式

Error 對象很少在代碼中明示地被創(chuàng)建,它們通常在拋出異常時自動地被創(chuàng)建。它們可以由 new Error(..) 構造形式創(chuàng)建,但通常是不必要的。

內容

正如剛才提到的,對象的內容由存儲在特定命名的 位置 上的(任意類型的)值組成,我們稱這些值為屬性。

有一個重要的事情需要注意:當我們說“內容”時,似乎暗示著這些值 實際上 存儲在對象內部,但那只不過是表面現(xiàn)象。引擎會根據(jù)自己的實現(xiàn)來存儲這些值,而且通常都不是把它們存儲在容器對象 內部。在容器內存儲的是這些屬性的名稱,它們像指針(技術上講,叫 引用(reference))一樣指向值存儲的地方。

考慮下面的代碼:

var myObject = {
    a: 2
};

myObject.a;     // 2

myObject["a"];  // 2

為了訪問 myObject位置 a 的值,我們需要使用 .[ ] 操作符。.a 語法通常稱為“屬性(property)”訪問,而 ["a"] 語法通常稱為“鍵(key)”訪問。在現(xiàn)實中,它們倆都訪問相同的 位置,而且會拿出相同的值,2,所以這些術語可以互換使用。從現(xiàn)在起,我們將使用最常見的術語 —— “屬性訪問”。

兩種語法的主要區(qū)別在于,. 操作符后面需要一個 標識符(Identifier) 兼容的屬性名,而 [".."] 語法基本可以接收任何兼容 UTF-8/unicode 的字符串作為屬性名。舉個例子,為了引用一個名為“Super-Fun!”的屬性,你不得不使用 ["Super-Fun!"] 語法訪問,因為 Super-Fun! 不是一個合法的 Identifier 屬性名。

而且,由于 [".."] 語法使用字符串的 來指定位置,這意味著程序可以動態(tài)地組建字符串的值。比如:

var wantA = true;
var myObject = {
    a: 2
};

var idx;

if (wantA) {
    idx = "a";
}

// 稍后

console.log( myObject[idx] ); // 2

在對象中,屬性名 總是 字符串。如果你使用 string 以外的(基本)類型值,它會首先被轉換為字符串。這甚至包括在數(shù)組中常用于索引的數(shù)字,所以要小心不要將對象和數(shù)組使用的數(shù)字搞混了。

var myObject = { };

myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";

myObject["true"];               // "foo"
myObject["3"];                  // "bar"
myObject["[object Object]"];    // "baz"

計算型屬性名

如果你需要將一個計算表達式 作為 一個鍵名稱,那么我們剛剛描述的 myObject[..] 屬性訪問語法是十分有用的,比如 myObject[prefix + name]。但是當使用字面對象語法聲明對象時則沒有什么幫助。

ES6 加入了 計算型屬性名,在一個字面對象聲明的鍵名稱位置,你可以指定一個表達式,用 [ ] 括起來:

var prefix = "foo";

var myObject = {
    [prefix + "bar"]: "hello",
    [prefix + "baz"]: "world"
};

myObject["foobar"]; // hello
myObject["foobaz"]; // world

計算型屬性名 的最常見用法,可能是用于 ES6 的 Symbol,我們將不會在本書中涵蓋關于它的細節(jié)。簡單地說,它們是新的基本數(shù)據(jù)類型,擁有一個不透明不可知的值(技術上講是一個 string 值)。你將會被強烈地不鼓勵使用一個 Symbol實際值 (這個值理論上會因 JS 引擎的不同而不同),所以 Symbol 的名稱,比如 Symbol.Something(這是個瞎編的名稱?。?,才是你會使用的:

var myObject = {
    [Symbol.Something]: "hello world"
};

屬性(Property) vs. 方法(Method)

有些開發(fā)者喜歡在討論對一個對象的屬性訪問時做一個區(qū)別,如果這個被訪問的值恰好是一個函數(shù)的話。因為這誘使人們認為函數(shù) 屬于 這個對象,而且在其他語言中,屬于對象(也就是“類”)的函數(shù)被稱作“方法”,所以相對于“屬性訪問”,我們常能聽到“方法訪問”。

有趣的是,語言規(guī)范也做出了同樣的區(qū)別

從技術上講,函數(shù)絕不會“屬于”對象,所以,說一個偶然在對象的引用上被訪問的函數(shù)就自動地成為了一個“方法”,看起來有些像是牽強附會。

有些函數(shù)內部確實擁有 this 引用,而且 有時 這些 this 引用指向調用點的對象引用。但這個用法確實沒有使這個函數(shù)比其他函數(shù)更像“方法”,因為 this 是在運行時在調用點動態(tài)綁定的,這使得它與這個對象的關系至多是間接的。

每次你訪問一個對象的屬性都是一個 屬性訪問,無論你得到什么類型的值。如果你 恰好 從屬性訪問中得到一個函數(shù),它也沒有魔法般地在那時成為一個“方法”。一個從屬性訪問得來的函數(shù)沒有任何特殊性(隱含的 this 綁定的情況在剛才已經解釋過了)。

舉個例子:

function foo() {
    console.log( "foo" );
}

var someFoo = foo;  // 對 `foo` 的變量引用


var myObject = {
    someFoo: foo
};

foo;                // function foo(){..}

someFoo;            // function foo(){..}

myObject.someFoo;   // function foo(){..}

someFoomyObject.someFoo 只不過是同一個函數(shù)的兩個分離的引用,它們中的任何一個都不意味著這個函數(shù)很特別或被其他對象所“擁有”。如果上面的 foo() 定義里面擁有一個 this 引用,那么 myObject.someFoo隱含綁定 將會是這個兩個引用間 唯一 可以觀察到的不同。它們中的任何一個都沒有稱為“方法”的道理。

也許有人會爭辯,函數(shù) 變成了方法,不是在定義期間,而是在調用的執(zhí)行期間,根據(jù)它是如何在調用點被調用的(是否帶有一個環(huán)境對象引用 —— 細節(jié)見第二章)。即便是這種解讀也有些牽強。

可能最安全的結論是,在 JavaScript 中,“函數(shù)”和“方法”是可以互換使用的。

注意: ES6 加入了 super 引用,它通常是和 class(見附錄A)一起使用的。super 的行為方式(靜態(tài)綁定,而非像 this 一樣延遲綁定),給了這種說法更多的權重:一個被 super 綁定到某處的函數(shù)比起“函數(shù)”更像一個“方法”。但是同樣地,這僅僅是微妙的語義上的(和機制上的)細微區(qū)別。

就算你聲明一個函數(shù)表達式作為字面對象的一部分,那個函數(shù)都不會魔法般地 屬于 這個對象 —— 仍然僅僅是同一個函數(shù)對象的多個引用罷了。

var myObject = {
    foo: function foo() {
        console.log( "foo" );
    }
};

var someFoo = myObject.foo;

someFoo;        // function foo(){..}

myObject.foo;   // function foo(){..}

注意: 在第六章中,我們會為字面對象的 foo: function foo(){ .. } 聲明語法介紹一種ES6的簡化語法。

數(shù)組

數(shù)組也使用 [ ] 訪問形式,但正如上面提到的,在存儲值的方式和位置上它們的組織更加結構化(雖然仍然在存儲值的 類型 上沒有限制)。數(shù)組采用 數(shù)字索引,這意味著值被存儲的位置,通常稱為 下標,是一個非負整數(shù),比如 042

var myArray = [ "foo", 42, "bar" ];

myArray.length;     // 3

myArray[0];         // "foo"

myArray[2];         // "bar"

數(shù)組也是對象,所以雖然每個索引都是正整數(shù),你還可以在數(shù)組上添加屬性:

var myArray = [ "foo", 42, "bar" ];

myArray.baz = "baz";

myArray.length; // 3

myArray.baz;    // "baz"

注意,添加命名屬性(不論是使用 . 還是 [ ] 操作符語法)不會改變數(shù)組的 length 所報告的值。

可以 把一個數(shù)組當做普通的鍵/值對象使用,并且從不添加任何數(shù)字下標,但這不是一個好主意,因為數(shù)組對它本來的用途有著特定的行為和優(yōu)化方式,普通對象也一樣。使用對象來存儲鍵/值對,而用數(shù)組在數(shù)字下標上存儲值。

小心: 如果你試圖在一個數(shù)組上添加屬性,但是屬性名 看起來 像一個數(shù)字,那么最終它會成為一個數(shù)字索引(也就是改變了數(shù)組的內容):

var myArray = [ "foo", 42, "bar" ];

myArray["3"] = "baz";

myArray.length; // 4

myArray[3];     // "baz"

復制對象

當開發(fā)者們初次拿起 Javascript 語言時,最常需要的特性就是如何復制一個對象??雌饋響撚幸粋€內建的 copy() 方法,對吧?但是事情實際上比這復雜一些,因為在默認情況下,復制的算法應當是什么,并不十分明確。

例如,考慮這個對象:

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

var anotherObject = {
    c: true
};

var anotherArray = [];

var myObject = {
    a: 2,
    b: anotherObject,   // 引用,不是拷貝!
    c: anotherArray,    // 又一個引用!
    d: anotherFunction
};

anotherArray.push( anotherObject, myObject );

一個myObject拷貝 究竟應該怎么表現(xiàn)?

首先,我們應該回答它是一個 淺(shallow) 還是一個 深(deep) 拷貝?一個 淺拷貝(shallow copy) 會得到一個新對象,它的 a 是值 2 的拷貝,但 b、cd 屬性僅僅是引用,它們指向被拷貝對象中引用的相同位置。一個 深拷貝(deep copy) 將不僅復制 myObject,還會復制 anotherObjectanotherArray。但之后我們讓 anotherArray 擁有 anotherObjectmyObject 的引用,所以 那些 也應當被復制而不是僅保留引用?,F(xiàn)在由于循環(huán)引用,我們得到了一個無限循環(huán)復制的問題。

我們應當檢測循環(huán)引用并打破循環(huán)遍歷嗎(不管位于深處的,沒有完全復制的元素)?我們應當報錯退出嗎?或者介于兩者之間?

另外,“復制”一個函數(shù)意味著什么,也不是很清楚。有一些技巧,比如提取一個函數(shù)源代碼的 toString() 序列化表達(這個源代碼會因實現(xiàn)不同而不同,而且根據(jù)被考察的函數(shù)的類型,其結果甚至在所有引擎上都不可靠)。

那么我們如何解決所有這些刁鉆的問題?不同的 JS 框架都各自挑選自己的解釋并且做出自己的選擇。但是哪一種(如果有的話)才是 JS 應當作為標準采用的呢?長久以來,沒有明確答案。

一個解決方案是,JSON 安全的對象(也就是,可以被序列化為一個 JSON 字符串,之后還可以被重新解析為擁有相同的結構和值的對象)可以簡單地這樣 復制

var newObj = JSON.parse( JSON.stringify( someObj ) );

當然,這要求你保證你的對象是 JSON 安全的。對于某些情況,這沒什么大不了的。而對另一些情況,這還不夠。

同時,淺拷貝相當易懂,而且沒有那么多問題,所以 ES6 為此任務已經定義了 Object.assign(..)。Object.assign(..) 接收 目標 對象作為第一個參數(shù),然后是一個或多個 對象作為后續(xù)參數(shù)。它會在 對象上迭代所有的 可枚舉(enumerable)owned keys直接擁有的鍵),并把它們拷貝到 目標 對象上(僅通過 = 賦值)。它還會很方便地返回 目標 對象,正如下面你可以看到的:

var newObj = Object.assign( {}, myObject );

newObj.a;                       // 2
newObj.b === anotherObject;     // true
newObj.c === anotherArray;      // true
newObj.d === anotherFunction;   // true

注意: 在下一部分中,我們將討論“屬性描述符(property descriptors —— 屬性的性質)”并展示 Object.defineProperty(..) 的使用。然而在 Object.assign(..) 中發(fā)生的復制是單純的 = 式賦值,所以任何在源對象屬性的特殊性質(比如 writable)在目標對象上 都不會保留 。

屬性描述符(Property Descriptors)

在 ES5 之前,JavaScript 語言沒有給出直接的方法,讓你的代碼可以考察或描述屬性性質間的區(qū)別,比如屬性是否為只讀。

在 ES5 中,所有的屬性都用 屬性描述符(Property Descriptors) 來描述。

考慮這段代碼:

var myObject = {
    a: 2
};

Object.getOwnPropertyDescriptor( myObject, "a" );
// {
//    value: 2,
//    writable: true,
//    enumerable: true,
//    configurable: true
// }

正如你所見,我們普通的對象屬性 a 的屬性描述符(稱為“數(shù)據(jù)描述符”,因為它僅持有一個數(shù)據(jù)值)的內容要比 value2 多得多。它還包含另外三個性質:writableenumerable、和 configurable。

當我們創(chuàng)建一個普通屬性時,可以看到屬性描述符的各種性質的默認值,同時我們可以用 Object.defineProperty(..) 來添加新屬性,或使用期望的性質來修改既存的屬性(如果它是 configurable 的!)。

舉例來說:

var myObject = {};

Object.defineProperty( myObject, "a", {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
} );

myObject.a; // 2

使用 defineProperty(..),我們手動、明確地在 myObject 上添加了一個直白的,普通的 a 屬性。然而,你通常不會使用這種手動方法,除非你想要把描述符的某個性質修改為不同的值。

可寫性(Writable)

writable 控制著你改變屬性值的能力。

考慮這段代碼:

var myObject = {};

Object.defineProperty( myObject, "a", {
    value: 2,
    writable: false, // 不可寫!
    configurable: true,
    enumerable: true
} );

myObject.a = 3;

myObject.a; // 2

如你所見,我們對 value 的修改悄無聲息地失敗了。如果我們在 strict mode 下進行嘗試,會得到一個錯誤:

"use strict";

var myObject = {};

Object.defineProperty( myObject, "a", {
    value: 2,
    writable: false, // 不可寫!
    configurable: true,
    enumerable: true
} );

myObject.a = 3; // TypeError

這個 TypeError 告訴我們,我們不能改變一個不可寫屬性。

注意: 我們一會兒就會討論 getters/setters,但是簡單地說,你可以觀察到 writable:false 意味著值不可改變,和你定義一個空的 setter 是有些等價的。實際上,你的空 setter 在被調用時需要扔出一個 TypeError,來和 writable:false 保持一致。

可配置性(Configurable)

只要屬性當前是可配置的,我們就可以使用相同的 defineProperty(..) 工具,修改它的描述符定義。

var myObject = {
    a: 2
};

myObject.a = 3;
myObject.a;                 // 3

Object.defineProperty( myObject, "a", {
    value: 4,
    writable: true,
    configurable: false,    // 不可配置!
    enumerable: true
} );

myObject.a;                 // 4
myObject.a = 5;
myObject.a;                 // 5

Object.defineProperty( myObject, "a", {
    value: 6,
    writable: true,
    configurable: true,
    enumerable: true
} ); // TypeError

最后的 defineProperty(..) 調用導致了一個 TypeError,這與 strict mode 無關,如果你試圖改變一個不可配置屬性的描述符定義,就會發(fā)生 TypeError。要小心:如你所看到的,將 configurable 設置為 false一個單向操作,不可撤銷!

注意: 這里有一個需要注意的微小例外:即便屬性已經是 configurable:false,writable 總是可以沒有錯誤地從 true 改變?yōu)?false,但如果已經是 false 的話不能變回 true

configurable:false 阻止的另外一個事情是使用 delete 操作符移除既存屬性的能力。

var myObject = {
    a: 2
};

myObject.a;             // 2
delete myObject.a;
myObject.a;             // undefined

Object.defineProperty( myObject, "a", {
    value: 2,
    writable: true,
    configurable: false,
    enumerable: true
} );

myObject.a;             // 2
delete myObject.a;
myObject.a;             // 2

如你所見,最后的 delete 調用(無聲地)失敗了,因為我們將 a 屬性設置成了不可配置。

delete 僅用于直接從目標對象移除該對象的(可以被移除的)屬性。如果一個對象的屬性是某個其他對象/函數(shù)的最后一個現(xiàn)存的引用,而你 delete 了它,那么這就移除了這個引用,于是現(xiàn)在那個沒有被任何地方所引用的對象/函數(shù)就可以被作為垃圾回收。但是,將 delete 當做一個像其他語言(如 C/C++)中那樣的釋放內存工具是 恰當?shù)摹?code>delete 僅僅是一個對象屬性移除操作 —— 沒有更多別的含義。

可枚舉性(Enumerable)

我們將要在這里提到的最后一個描述符性質是 enumerable(還有另外兩個,我們將在一會兒討論 getter/setters 時談到)。

它的名稱可能已經使它的功能很明顯了,這個性質控制著一個屬性是否能在特定的對象-屬性枚舉操作中出現(xiàn),比如 for..in 循環(huán)。設置為 false 將會阻止它出現(xiàn)在這樣的枚舉中,即使它依然完全是可以訪問的。設置為 true 會使它出現(xiàn)。

所有普通的用戶定義屬性都默認是可 enumerable 的,正如你通常希望的那樣。但如果你有一個特殊的屬性,你想讓它對枚舉隱藏,就將它設置為 enumerable:false。

我們一會兒就更加詳細地演示可枚舉性,所以在大腦中給這個話題上打一個書簽。

不可變性(Immutability)

有時我們希望將屬性或對象(有意或無意地)設置為不可改變的。ES5 用幾種不同的微妙方式,加入了對此功能的支持。

一個重要的注意點是:所有 這些方法創(chuàng)建的都是淺不可變性。也就是,它們僅影響對象和它的直屬屬性的性質。如果對象擁有對其他對象(數(shù)組、對象、函數(shù)等)的引用,那個對象的 內容 不會受影響,任然保持可變。

myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push( 4 );
myImmutableObject.foo; // [1,2,3,4]

在這段代碼中,我們假設 myImmutableObject 已經被創(chuàng)建,而且被保護為不可變。但是,為了保護 myImmutableObject.foo 的內容(也是一個對象 —— 數(shù)組),你將需要使用下面的一個或多個方法將 foo 設置為不可變。

注意: 在 JS 程序中創(chuàng)建完全不可動搖的對象是不那么常見的。有些特殊情況當然需要,但作為一個普通的設計模式,如果你發(fā)現(xiàn)自己想要 封?。╯eal)凍結(freeze) 你所有的對象,那么你可能想要退一步來重新考慮你的程序設計,讓它對對象值的潛在變化更加健壯。

對象常量(Object Constant)

通過將 writable:falseconfigurable:false 組合,你可以實質上創(chuàng)建了一個作為對象屬性的 常量(不能被改變,重定義或刪除),比如:

var myObject = {};

Object.defineProperty( myObject, "FAVORITE_NUMBER", {
    value: 42,
    writable: false,
    configurable: false
} );

防止擴展(Prevent Extensions)

如果你想防止一個對象被添加新的屬性,但另一方面保留其他既存的對象屬性,可以調用 Object.preventExtensions(..)

var myObject = {
    a: 2
};

Object.preventExtensions( myObject );

myObject.b = 3;
myObject.b; // undefined

在非 strict mode 模式下,b 的創(chuàng)建會無聲地失敗。在 strict mode 下,它會拋出 TypeError。

封?。⊿eal)

Object.seal(..) 創(chuàng)建一個“封印”的對象,這意味著它實質上在當前的對象上調用 Object.preventExtensions(..),同時也將它所有的既存屬性標記為 configurable:false。

所以,你既不能添加更多的屬性,也不能重新配置或刪除既存屬性(雖然你依然 可以 修改它們的值)。

凍結(Freeze)

Object.freeze(..) 創(chuàng)建一個凍結的對象,這意味著它實質上在當前的對象上調用 Object.seal(..),同時也將它所有的“數(shù)據(jù)訪問”屬性設置為 writable:false,所以它們的值不可改變。

這種方法是你可以從對象自身獲得的最高級別的不可變性,因為它阻止任何對對象或對象直屬屬性的改變(雖然,就像上面提到的,任何被引用的對象的內容不受影響)。

你可以“深度凍結”一個對象:在這個對象上調用 Object.freeze(..),然后遞歸地迭代所有它引用的(目前還沒有受過影響的)對象,然后也在它們上面調用 Object.freeze(..)。但是要小心,這可能會影響其他你并不打算影響的(共享的)對象。

[[Get]]

關于屬性訪問如何工作有一個重要的細節(jié)。

考慮下面的代碼:

var myObject = {
    a: 2
};

myObject.a; // 2

myObject.a 是一個屬性訪問,但是它并不是看起來那樣,僅僅在 myObject 中尋找一個名為 a 的屬性。

根據(jù)語言規(guī)范,上面的代碼實際上在 myObject 上執(zhí)行了一個 [[Get]] 操作(有些像 [[Get]]() 函數(shù)調用)。對一個對象進行默認的內建 [[Get]] 操作,會 首先 檢查對象,尋找一個擁有被請求的名稱的屬性,如果找到,就返回相應的值。

然而,如果按照被請求的名稱 沒能 找到屬性,[[Get]] 的算法定義了另一個重要的行為。我們會在第五章來解釋 接下來 會發(fā)生什么(遍歷 [[Prototype]] 鏈,如果有的話)。

[[Get]] 操作的一個重要結果是,如果它通過任何方法都不能找到被請求的屬性的值,那么它會返回 undefined

var myObject = {
    a: 2
};

myObject.b; // undefined

這個行為和你通過標識符名稱來引用 變量 不同。如果你引用了一個在可用的詞法作用域內無法解析的變量,其結果不是像對象屬性那樣返回 undefined,而是拋出一個 ReferenceError。

var myObject = {
    a: undefined
};

myObject.a; // undefined

myObject.b; // undefined

的角度來說,這兩個引用沒有區(qū)別 —— 它們的結果都是 undefined。然而,在 [[Get]] 操作的底層,雖然不明顯,但是比起處理引用 myObject.a,處理 myObject.b 的操作要多做一些潛在的“工作”。

如果僅僅考察結果的值,你無法分辨一個屬性是存在并持有一個 undefined 值,還是因為屬性根本 存在所以 [[Get]] 無法返回某個具體值而返回默認的 undefined。但是,你很快就能看到你其實 可以 分辨這兩種場景。

[[Put]]

既然為了從一個屬性中取得值而存在一個內部定義的 [[Get]] 操作,那么很明顯應該也存在一個默認的 [[Put]] 操作。

這很容易讓人認為,給一個對象的屬性賦值,將會在這個對象上調用 [[Put]] 來設置或創(chuàng)建這個屬性。但是實際情況卻有一些微妙的不同。

調用 [[Put]] 時,它根據(jù)幾個因素表現(xiàn)不同的行為,包括(影響最大的)屬性是否已經在對象中存在了。

如果屬性存在,[[Put]] 算法將會大致檢查:

  1. 這個屬性是訪問器描述符嗎(見下一節(jié)"Getters 與 Setters")?如果是,而且是 setter,就調用 setter。
  2. 這個屬性是 writablefalse 數(shù)據(jù)描述符嗎?如果是,在非 strict mode 下無聲地失敗,或者在 strict mode 下拋出 TypeError
  3. 否則,像平常一樣設置既存屬性的值。

如果屬性在當前的對象中還不存在,[[Put]] 操作會變得更微妙和復雜。我們將在第五章討論 [[Prototype]] 時再次回到這個場景,更清楚地解釋它。

Getters 與 Setters

對象默認的 [[Put]][[Get]] 操作分別完全控制著如何設置既存或新屬性的值,和如何取得既存屬性。

注意: 使用較先進的語言特性,覆蓋整個對象(不僅是每個屬性)的默認 [[Put]][[Get]] 操作是可能的。這超出了我們要在這本書中討論的范圍,但我們會在后面的“你不懂 JS”系列中涵蓋此內容。

ES5 引入了一個方法來覆蓋這些默認操作的一部分,但不是在對象級別而是針對每個屬性,就是通過 getters 和 setters。Getter 是實際上調用一個隱藏函數(shù)來取得值的屬性。Setter 是實際上調用一個隱藏函數(shù)來設置值的屬性。

當你將一個屬性定義為擁有 getter 或 setter 或兩者兼?zhèn)?,那么它的定義就成為了“訪問器描述符”(與“數(shù)據(jù)描述符”相對)。對于訪問器描述符,它的 valuewritable 性質因沒有意義而被忽略,取而代之的是 JS 將會考慮屬性的 setget 性質(還有 configurableenumerable)。

考慮下面的代碼:

var myObject = {
    // 為 `a` 定義一個 getter
    get a() {
        return 2;
    }
};

Object.defineProperty(
    myObject,   // 目標對象
    "b",        // 屬性名
    {           // 描述符
        // 為 `b` 定義 getter
        get: function(){ return this.a * 2 },

        // 確保 `b` 作為對象屬性出現(xiàn)
        enumerable: true
    }
);

myObject.a; // 2

myObject.b; // 4

不管是通過在字面對象語法中使用 get a() { .. },還是通過使用 defineProperty(..) 明確定義,我們都在對象上創(chuàng)建了一個沒有實際持有值的屬性,訪問它們將會自動地對 getter 函數(shù)進行隱藏的函數(shù)調用,其返回的任何值就是屬性訪問的結果。

var myObject = {
    // 為 `a` 定義 getter
    get a() {
        return 2;
    }
};

myObject.a = 3;

myObject.a; // 2

因為我們僅為 a 定義了一個 getter,如果之后我們試著設置 a 的值,賦值操作并不會拋出錯誤而是無聲地將賦值廢棄。就算這里有一個合法的 setter,我們的自定義 getter 將返回值硬編碼為僅返回 2,所以賦值操作是沒有意義的。

為了使這個場景更合理,正如你可能期望的那樣,每個屬性還應當被定義一個覆蓋默認 [[Put]] 操作(也就是賦值)的 setter。幾乎可確定,你將總是想要同時聲明 getter 和 setter(僅有它們中的一個經常會導致意外的行為):

var myObject = {
    // 為 `a` 定義 getter
    get a() {
        return this._a_;
    },

    // 為 `a` 定義 setter
    set a(val) {
        this._a_ = val * 2;
    }
};

myObject.a = 2;

myObject.a; // 4

注意: 在這個例子中,我們實際上將賦值操作([[Put]] 操作)指定的值 2 存儲到了另一個變量 _a_ 中。_a_ 這個名稱只是用在這個例子中的單純慣例,并不意味著它的行為有什么特別之處 —— 它和其他普通屬性沒有區(qū)別。

存在性(Existence)

我們早先看到,像 myObject.a 這樣的屬性訪問可能會得到一個 undefined 值,無論是它明確存儲著 undefined 還是屬性 a 根本就不存在。那么,如果這兩種情況的值相同,我們還怎么區(qū)別它們呢?

我們可以查詢一個對象是否擁有特定的屬性,而 不必 取得那個屬性的值:

var myObject = {
    a: 2
};

("a" in myObject);              // true
("b" in myObject);              // false

myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false

in 操作符會檢查屬性是否存在于對象 ,或者是否存在于 [[Prototype]] 鏈對象遍歷的更高層中(詳見第五章)。相比之下,hasOwnProperty(..) 僅僅 檢查 myObject 是否擁有屬性,但 不會 查詢 [[Prototype]] 鏈。我們會在第五章詳細講解 [[Prototype]] 時,回來討論這個兩個操作重要的不同。

通過委托到 Object.prototype,所有的普通對象都可以訪問 hasOwnProperty(..)(詳見第五章)。但是創(chuàng)建一個不鏈接到 Object.prototype 的對象也是可能的(通過 Object.create(null) —— 詳見第五章)。這種情況下,像 myObject.hasOwnProperty(..) 這樣的方法調用將會失敗。

在這種場景下,一個進行這種檢查的更健壯的方式是 Object.prototype.hasOwnProperty.call(myObject,"a"),它借用基本的 hasOwnProperty(..) 方法而且使用 明確的 this 綁定(詳見第二章)來對我們的 myObject 實施這個方法。

注意: in 操作符看起來像是要檢查一個值在容器中的存在性,但是它實際上檢查的是屬性名的存在性。在使用數(shù)組時注意這個區(qū)別十分重要,因為我們會有很強的沖動來進行 4 in [2, 4, 6] 這樣的檢查,但是這總是不像我們想象的那樣工作。

枚舉(Enumeration)

先前,在學習 enumerable 屬性描述符性質時,我們簡單地解釋了"可枚舉性(enumerability)"的含義?,F(xiàn)在,讓我們來更加詳細地重新講解它。

var myObject = { };

Object.defineProperty(
    myObject,
    "a",
    // 使 `a` 可枚舉,如一般情況
    { enumerable: true, value: 2 }
);

Object.defineProperty(
    myObject,
    "b",
    // 使 `b` 不可枚舉
    { enumerable: false, value: 3 }
);

myObject.b; // 3
("b" in myObject); // true
myObject.hasOwnProperty( "b" ); // true

// .......

for (var k in myObject) {
    console.log( k, myObject[k] );
}
// "a" 2

你會注意到,myObject.b 實際上 存在,而且擁有可以訪問的值,但是它不出現(xiàn)在 for..in 循環(huán)中(然而令人詫異的是,它的 in 操作符的存在性檢查通過了)。這是因為 “enumerable” 基本上意味著“如果對象的屬性被迭代時會被包含在內”。

注意:for..in 循環(huán)實施在數(shù)組上可能會給出意外的結果,因為枚舉一個數(shù)組將不僅包含所有的數(shù)字下標,還包含所有的可枚舉屬性。所以一個好主意是:將 for..in 循環(huán) 用于對象,而為存儲在數(shù)組中的值使用傳統(tǒng)的 for 循環(huán)并用數(shù)字索引迭代。

另一個可以區(qū)分可枚舉和不可枚舉屬性的方法是:

var myObject = { };

Object.defineProperty(
    myObject,
    "a",
    // 使 `a` 可枚舉,如一般情況
    { enumerable: true, value: 2 }
);

Object.defineProperty(
    myObject,
    "b",
    // 使 `b` 不可枚舉
    { enumerable: false, value: 3 }
);

myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false

Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]

propertyIsEnumerable(..) 測試一個給定的屬性名是否直 接存 在于對象上,并且是 enumerable:true

Object.keys(..) 返回一個所有可枚舉屬性的數(shù)組,而 Object.getOwnPropertyNames(..) 返回一個 所有 屬性的數(shù)組,不論能不能枚舉。

inhasOwnProperty(..) 區(qū)別于它們是否查詢 [[Prototype]] 鏈,而 Object.keys(..)Object.getOwnPropertyNames(..) 考察直接給定的對象。

(當下)沒有與 in 操作符的查詢方式(在整個 [[Prototype]] 鏈上遍歷所有的屬性,如我們在第五章解釋的)等價的、內建的方法可以得到一個 所有屬性 的列表。你可以近似地模擬一個這樣的工具:遞歸地遍歷一個對象的 [[Prototype]] 鏈,在每一層都從 Object.keys(..) 中取得一個列表——僅包含可枚舉屬性。

迭代(Iteration)

for..in 循環(huán)迭代一個對象上(包括它的 [[Prototype]] 鏈)所有的可迭代屬性。但如果你想要迭代值呢?

在數(shù)字索引的數(shù)組中,典型的迭代所有的值的辦法是使用標準的 for 循環(huán),比如:

var myArray = [1, 2, 3];

for (var i = 0; i < myArray.length; i++) {
    console.log( myArray[i] );
}
// 1 2 3

但是這并沒有迭代所有的值,而是迭代了所有的下標,然后由你使用索引來引用值,比如 myArray[i]。

ES5 還為數(shù)組加入了幾個迭代幫助方法,包括 forEach(..)、every(..)、和 some(..)。這些幫助方法的每一個都接收一個回調函數(shù),這個函數(shù)將施用于數(shù)組中的每一個元素,僅在如何響應回調的返回值上有所不同。

forEach(..) 將會迭代數(shù)組中所有的值,并且忽略回調的返回值。every(..) 會一直迭代到最后,或者 當回調返回一個 false(或“falsy”)值,而 some(..) 會一直迭代到最后,或者 當回調返回一個 true(或“truthy”)值。

這些在 every(..)some(..) 內部的特殊返回值有些像普通 for 循環(huán)中的 break 語句,它們可以在迭代執(zhí)行到末尾之前將它結束掉。

如果你使用 for..in 循環(huán)在一個對象上進行迭代,你也只能間接地得到值,因為它實際上僅僅迭代對象的所有可枚舉屬性,讓你自己手動地去訪問屬性來得到值。

注意: 與以有序數(shù)字的方式(for 循環(huán)或其他迭代器)迭代數(shù)組的下標比較起來,迭代對象屬性的順序是 不確定 的,而且可能會因 JS 引擎的不同而不同。對于需要跨平臺環(huán)境保持一致的問題,不要依賴 觀察到的順序,因為這個順序是不可靠的。

但是如果你想直接迭代值,而不是數(shù)組下標(或對象屬性)呢?ES6 加入了一個有用的 for..of 循環(huán)語法,用來迭代數(shù)組(和對象,如果這個對象有定義的迭代器):

var myArray = [ 1, 2, 3 ];

for (var v of myArray) {
    console.log( v );
}
// 1
// 2
// 3

for..of 循環(huán)要求被迭代的 東西 提供一個迭代器對象(從一個在語言規(guī)范中叫做 @@iterator 的默認內部函數(shù)那里得到),每次循環(huán)都調用一次這個迭代器對象的 next() 方法,循環(huán)迭代的內容就是這些連續(xù)的返回值。

數(shù)組擁有內建的 @@iterator,所以正如展示的那樣,for..of 對于它們很容易使用。但是讓我們使用內建的 @@iterator 來手動迭代一個數(shù)組,來看看它是怎么工作的:

var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();

it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }

注意: 我們使用一個 ES6 的 SymbolSymbol.iterator 來取得一個對象的 @@iterator 內部屬性。我們在本章中簡單地提到過 Symbol 的語義(見“計算型屬性名”),同樣的原理也適用于這里。你總是希望通過 Symbol 名稱,而不是它可能持有的特殊的值,來引用這樣特殊的屬性。另外,盡管這個名稱有這樣的暗示,但 @@iterator 本身 不是迭代器對象, 而是一個返回迭代器對象的 方法 —— 一個重要的細節(jié)!

正如上面的代碼段揭示的,迭代器的 next() 調用的返回值是一個 { value: .. , done: .. } 形式的對象,其中 value 是當前迭代的值,而 done 是一個 boolean,表示是否還有更多內容可以迭代。

注意值 3done:false 一起返回,猛地一看會有些奇怪。你不得不第四次調用 next()(在前一個代碼段的 for..of 循環(huán)會自動這樣做)來得到 done:true,以使自己知道迭代已經完成。這個怪異之處的原因超出了我們要在這里討論的范圍,但是它源自于 ES6 生成器(generator)函數(shù)的語義。

雖然數(shù)組可以在 for..of 循環(huán)中自動迭代,但普通的對象 沒有內建的 @@iterator。這種故意省略的原因要比我們將在這里解釋的更復雜,但一般來說,為了未來的對象類型,最好不要加入那些可能最終被證明是麻煩的實現(xiàn)。

但是 可以 為你想要迭代的對象定義你自己的默認 @@iterator。比如:

var myObject = {
    a: 2,
    b: 3
};

Object.defineProperty( myObject, Symbol.iterator, {
    enumerable: false,
    writable: false,
    configurable: true,
    value: function() {
        var o = this;
        var idx = 0;
        var ks = Object.keys( o );
        return {
            next: function() {
                return {
                    value: o[ks[idx++]],
                    done: (idx > ks.length)
                };
            }
        };
    }
} );

// 手動迭代 `myObject`
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefined, done:true }

// 用 `for..of` 迭代 `myObject`
for (var v of myObject) {
    console.log( v );
}
// 2
// 3

注意: 我們使用了 Object.defineProperty(..) 來自定義我們的 @@iterator(很大程度上是因為我們可以將它指定為不可枚舉的),但是通過將 Symbol 作為一個 計算型屬性名(在本章前面的部分討論過),我們也可以直接聲明它,比如 var myObject = { a:2, b:3, [Symbol.iterator]: function(){ /* .. */ } }

每次 for..of 循環(huán)在 myObject 的迭代器對象上調用 next() 時,迭代器內部的指針將會向前移動并返回對象屬性列表的下一個值(關于對象屬性/值迭代順序,參照前面的注意事項)。

我們剛剛演示的迭代,是一個簡單的一個值一個值的迭代,當然你可以為你的自定義數(shù)據(jù)結構定義任意復雜的迭代方法,只要你覺得合適。對于操作用戶自定義對象來說,自定義迭代器與 ES6 的 for..of 循環(huán)相組合,是一個新的強大的語法工具。

舉個例子,一個 Pixel(像素) 對象列表(擁有 xy 的坐標值)可以根據(jù)距離原點 (0,0) 的直線距離決定它的迭代順序,或者過濾掉那些“太遠”的點,等等。只要你的迭代器從 next() 調用返回期望的 { value: .. } 返回值,并在迭代結束后返回一個 { done: true } 值,ES6 的 for..of 循環(huán)就可以迭代它。

其實,你甚至可以生成一個永遠不會“結束”,并且總會返回一個新值(比如隨機數(shù),遞增值,唯一的識別符等等)的“無窮”迭代器,雖然你可能不會將這樣的迭代器用于一個沒有邊界的 for..of 循環(huán),因為它永遠不會結束,而且會阻塞你的程序。

var randoms = {
    [Symbol.iterator]: function() {
        return {
            next: function() {
                return { value: Math.random() };
            }
        };
    }
};

var randoms_pool = [];
for (var n of randoms) {
    randoms_pool.push( n );

    // 不要超過邊界!
    if (randoms_pool.length === 100) break;
}

這個迭代器會“永遠”生成隨機數(shù),所以我們小心地僅從中取出 100 個值,以使我們的程序不被阻塞。

復習

JS 中的對象擁有字面形式(比如 var a = { .. })和構造形式(比如 var a = new Array(..))。字面形式幾乎總是首選,但在某些情況下,構造形式提供更多的構建選項。

許多人聲稱“Javascript 中的一切都是對象”,這是不對的。對象是六種(或七中,看你從哪個方面說)基本類型之一。對象有子類型,包括 function,還可以被行為特化,比如 [object Array] 作為內部的標簽表示子類型數(shù)組。

對象是鍵/值對的集合。通過 .propName["propName"] 語法,值可以作為屬性訪問。不管屬性什么時候被訪問,引擎實際上會調用內部默認的 [[Get]] 操作(在設置值時調用 [[Put]] 操作),它不僅直接在對象上查找屬性,在沒有找到時還會遍歷 [[Prototype]] 鏈(見第五章)。

屬性有一些可以通過屬性描述符控制的特定性質,比如 writableconfigurable。另外,對象擁有它的不可變性(它們的屬性也有),可以通過使用 Object.preventExtensions(..)Object.seal(..)、和 Object.freeze(..) 來控制幾種不同等級的不可變性。

屬性不必非要包含值 —— 它們也可以是帶有 getter/setter 的“訪問器屬性”。它們也可以是可枚舉或不可枚舉的,這控制它們是否會在 for..in 這樣的循環(huán)迭代中出現(xiàn)。

你也可以使用 ES6 的 for..of 語法,在數(shù)據(jù)結構(數(shù)組,對象等)中迭代 ,它尋找一個內建或自定義的 @@iterator 對象,這個對象由一個 next() 方法組成,通過這個 next() 方法每次迭代一個數(shù)據(jù)。

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

推薦閱讀更多精彩內容