《你不知道的JavaScript(上)-原型對(duì)象》學(xué)習(xí)筆記

3.1 語(yǔ)法

  • 對(duì)象的文字語(yǔ)法:
var myObj = { 
    key: value
    // ... 
};
  • 構(gòu)造形式:
var myObj = new Object(); 
myObj.key = value;


3.2 類(lèi)型

  • 在 JavaScript 中一共有六種主要類(lèi)型:string、boolean、number、null酥泛、undefined今豆、object
  • 注意柔袁,簡(jiǎn)單基本類(lèi)型(string呆躲、boolean、number捶索、null插掂、undefined)本身并不是對(duì)象,不要與String腥例、Number辅甥、Boolean搞混。
  • null 有時(shí)會(huì)被當(dāng)作一種對(duì)象類(lèi)型燎竖,但是這其實(shí)只是語(yǔ)言本身的一個(gè) bug璃弄,即對(duì) null 執(zhí)行 typeof null 時(shí)會(huì)返回字符串 "object"。實(shí)際上构回,null 本身是基本類(lèi)型夏块。
  • 有一種常見(jiàn)的錯(cuò)誤說(shuō)法是“JavaScript 中萬(wàn)物皆是對(duì)象”,這顯然是錯(cuò)誤的纤掸。
  • 實(shí)際上脐供,JavaScript 中有許多特殊的對(duì)象子類(lèi)型,我們可以稱(chēng)之為復(fù)雜基本類(lèi)型茁肠。
  • 函數(shù)就是對(duì)象的一個(gè)子類(lèi)型(從技術(shù)角度來(lái)說(shuō)就是“可調(diào)用的對(duì)象”)患民。JavaScript 中的函數(shù)是“一等公民”缩举,因?yàn)樗鼈儽举|(zhì)上和普通的對(duì)象一樣(只是可以調(diào)用)垦梆,所以可以像操作其他對(duì)象一樣操作函數(shù)(比如當(dāng)作另一個(gè)函數(shù)的參數(shù))。
  • 數(shù)組也是對(duì)象的一種類(lèi)型仅孩,具備一些額外的行為托猩。數(shù)組中內(nèi)容的組織方式比一般的對(duì)象要 稍微復(fù)雜一些。

內(nèi)置對(duì)象
  • JavaScript 中還有一些對(duì)象子類(lèi)型辽慕,通常被稱(chēng)為內(nèi)置對(duì)象京腥。有些內(nèi)置對(duì)象的名字看起來(lái)和 簡(jiǎn)單基礎(chǔ)類(lèi)型一樣,不過(guò)實(shí)際上它們的關(guān)系更復(fù)雜溅蛉。
  • 內(nèi)置對(duì)象:String公浪、Number、Boolean船侧、Object欠气、Function、Array镜撩、Data预柒、RegExp、Error
    在 JavaScript 中宜鸯,它們實(shí)際上是一些內(nèi)置函數(shù)憔古。這些內(nèi)置函數(shù)可以當(dāng)作構(gòu)造函數(shù) (由 new 產(chǎn)生的函數(shù)調(diào)用)來(lái)使用,從而可以構(gòu)造一個(gè)對(duì)應(yīng)子類(lèi)型的新對(duì)象淋袖。
    舉例來(lái)說(shuō):
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
// 檢查 sub-type 對(duì)象
Object.prototype.toString.call( strObject ); // [object String]

原始值 "I am a string" 并不是一個(gè)對(duì)象鸿市,它只是一個(gè)字面量,并且是一個(gè)不可變的值即碗。 如果要在這個(gè)字面量上執(zhí)行一些操作灸芳,比如獲取長(zhǎng)度、訪(fǎng)問(wèn)其中某個(gè)字符等拜姿,那需要將其轉(zhuǎn)換為 String 對(duì)象烙样。
幸好,在必要時(shí)語(yǔ)言會(huì)自動(dòng)把字符串字面量轉(zhuǎn)換成一個(gè) String 對(duì)象蕊肥,也就是說(shuō)你并不需要顯式創(chuàng)建一個(gè)對(duì)象谒获。
例如:

var strPrimitive = "I am a string"; 
console.log( strPrimitive.length ); // 13 
console.log( strPrimitive.charAt( 3 ) ); // "m"

使用以上兩種方法,我們都可以直接在字符串字面量上訪(fǎng)問(wèn)屬性或者方法壁却,之所以可以這 樣做批狱,是因?yàn)橐孀詣?dòng)把字面量轉(zhuǎn)換成 String 對(duì)象,所以可以訪(fǎng)問(wèn)屬性和方法展东。

  • 因此赔硫,
    1.對(duì)于字符串字面量(string)、數(shù)值字面量(number)盐肃、布爾字面量(boolean)來(lái)說(shuō)爪膊,我們都可以直接在其上面訪(fǎng)問(wèn)屬性或者方法,之所以可以這樣做砸王,是因?yàn)橐孀詣?dòng)把字面量轉(zhuǎn)換成 String粹舵、Number互婿、Boolean對(duì)象,所以可以訪(fǎng)問(wèn)屬性和方法。
    2.null赵刑、undefined 沒(méi)有對(duì)應(yīng)的構(gòu)造形式槽袄,它們只有文字形式芹关。相反皿桑,Date 只有構(gòu)造(new Date(..)),沒(méi)有文字形式嘹朗。
    3.對(duì)于 Object师妙、Array、Function骡显、RegExp來(lái)說(shuō)疆栏,無(wú)論使用文字形式還是構(gòu)造形式曾掂,它們都是對(duì)象,不是字面量壁顶。
    4.Error 對(duì)象很少在代碼中顯式創(chuàng)建珠洗,一般是在拋出異常時(shí)被自動(dòng)創(chuàng)建。也可以使用 new Error(..) 這種構(gòu)造形式來(lái)創(chuàng)建若专,不過(guò)一般來(lái)說(shuō)用不著许蓖。

3.3 內(nèi)容(對(duì)象的屬性)

  • 對(duì)象的內(nèi)容是由一些存儲(chǔ)在特定命名位置的(任意類(lèi)型的)值組成的,我們稱(chēng)之為屬性调衰。
  • 需要強(qiáng)調(diào)的一點(diǎn)是膊爪,當(dāng)我們說(shuō)“內(nèi)容”時(shí),似乎在暗示這些值實(shí)際上被存儲(chǔ)在對(duì)象內(nèi)部嚎莉, 但是這只是它的表現(xiàn)形式米酬。在引擎內(nèi)部,這些值的存儲(chǔ)方式是多種多樣的趋箩,一般并不會(huì)存在對(duì)象容器內(nèi)部赃额。存儲(chǔ)在對(duì)象容器內(nèi)部的是這些屬性的名稱(chēng),它們就像指針(從技術(shù)角度來(lái)說(shuō)就是引用)一樣叫确,指向這些值真正的存儲(chǔ)位置跳芳。
  • 在對(duì)象中,屬性名永遠(yuǎn)都是字符串竹勉。如果你使用 string(字面量)以外的其他值作為屬性 名飞盆,那它首先會(huì)被轉(zhuǎn)換為一個(gè)字符串。即使是數(shù)字也不例外次乓,雖然在數(shù)組下標(biāo)中使用的的 確是數(shù)字吓歇,但是在對(duì)象屬性名中數(shù)字會(huì)被轉(zhuǎn)換成字符串,所以當(dāng)心不要搞混對(duì)象和數(shù)組中數(shù)字的用法:
var myObject = { };

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

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


3.3.1 可計(jì)算屬性名

ES6 增加了可計(jì)算屬性名檬输,可以在文字形式中使用 [] 包裹一個(gè)表達(dá)式來(lái)當(dāng)作屬性名:

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

可計(jì)算屬性名最常用的場(chǎng)景可能是 ES6 的符號(hào)(Symbol)照瘾。簡(jiǎn)單來(lái)說(shuō),Symbol是一種新的基礎(chǔ)數(shù)據(jù)類(lèi)型丧慈,包含一個(gè)不透明且無(wú)法預(yù)測(cè)的值(從技術(shù)角度來(lái)說(shuō)就是一個(gè)字符串)。一般來(lái)說(shuō)你不會(huì)用到符號(hào)的實(shí)際值(因?yàn)槔碚撋蟻?lái)說(shuō)在不 同的 JavaScript 引擎中值是不同的)主卫,所以通常你接觸到的是符號(hào)的名稱(chēng)逃默,比如 Symbol. Something(這個(gè)名字是我編的):

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


3.3.4 復(fù)制對(duì)象
  • Array:sliceconcat簇搅、Array.from()
  • Object: Object.assign()完域、JSON.parse(JSON.stringify(obj))
    不過(guò)使用JSON.parse(JSON.stringify(obj))的話(huà),undefined瘩将、任意的函數(shù)吟税、symbol在序列化過(guò)程中會(huì)被忽略(出現(xiàn)在非數(shù)組對(duì)象的屬性中時(shí))或者被轉(zhuǎn)換成null(出現(xiàn)在數(shù)組中時(shí))
3.3.5 屬性描述符

writable(可寫(xiě))凹耙、 enumerable(可枚舉)configurable(可配置)肠仪。

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

在創(chuàng)建普通屬性時(shí)屬性描述符會(huì)使用默認(rèn)值肖抱,我們也可以使用 Object.defineProperty(..) 來(lái)添加一個(gè)新屬性或者修改一個(gè)已有屬性(如果它是 configurable)并對(duì)特性進(jìn)行設(shè)置。
舉例來(lái)說(shuō):

var myObject = {}; 

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

writable 決定是否可以修改屬性的值异旧。
思考下面的代碼:

var myObject = {}; 
 
Object.defineProperty( myObject, "a", {     
    value: 2,     
    writable: false, // 不可寫(xiě)意述!     
    configurable: true,     
    enumerable: true 
} ); 
 
myObject.a = 3; 
 
myObject.a; // 2

如你所見(jiàn),我們對(duì)于屬性值的修改靜默失斔庇肌(silently failed)了荤崇。如果在嚴(yán)格模式下,這種方法會(huì)出錯(cuò):

"use strict"; 
 
var myObject = {}; 
 
Object.defineProperty( myObject, "a", {     
    value: 2,     
    writable: false, // 不可寫(xiě)潮针!     
    configurable: true,     
    enumerable: true 
} ); 
 
myObject.a = 3; // TypeError

TypeError 錯(cuò)誤表示我們無(wú)法修改一個(gè)不可寫(xiě)的屬性术荤。

2. Configurable

只要屬性是可配置的,就可以使用 defineProperty(..) 方法來(lái)修改屬性描述符

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

最后一個(gè) defineProperty(..) 會(huì)產(chǎn)生一個(gè) TypeError 錯(cuò)誤喜每,不管是不是處于嚴(yán)格模式,嘗 試修改一個(gè)不可配置的屬性描述符都會(huì)出錯(cuò)雳攘。注意:如你所見(jiàn)带兜,把 configurable 修改成 false 是單向操作,無(wú)法撤銷(xiāo)吨灭!

要注意有一個(gè)小小的例外:即便屬性是 configurable:false刚照, 我們還是可以 把 writable 的狀態(tài)由 true 改為 false,但是無(wú)法由 false 改為 true喧兄。

除了無(wú)法修改无畔,configurable:false 還會(huì)禁止刪除這個(gè)屬性:

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

如你所見(jiàn),最后一個(gè) delete 語(yǔ)句(靜默)失敗了吠冤,因?yàn)閷傩允遣豢膳渲玫摹?/p>

3. Enumerable

這個(gè)描述符控制的是屬性是否會(huì)出現(xiàn)在對(duì)象的屬性枚舉中浑彰,比如說(shuō) for..in 循環(huán)。如果把 enumerable 設(shè)置成 false拯辙,這個(gè)屬性就不會(huì)出現(xiàn)在枚舉中郭变,雖然仍然可以正常訪(fǎng)問(wèn)它。相對(duì)地涯保,設(shè)置成 true 就會(huì)讓它出現(xiàn)在枚舉中诉濒。
用戶(hù)定義的所有的普通屬性默認(rèn)都是 enumerable,這通常就是你想要的夕春。但是如果你不希 望某些特殊屬性出現(xiàn)在枚舉中未荒,那就把它設(shè)置成 enumerable:false。

3.3.6 不變性
1. 對(duì)象常量

結(jié)合 writable:falseconfigurable:false 就可以創(chuàng)建一個(gè)真正的常量屬性(不可修改及志、 重定義或者刪除):

var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
    value: 42,
    writable: false,
    configurable: false 
} );
2. 禁止擴(kuò)展

如果你想禁止一個(gè)對(duì)象添加新屬性并且保留已有屬性片排,可以使用 Object.prevent Extensions(..):

var myObject = { 
    a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined

在非嚴(yán)格模式下寨腔,創(chuàng)建屬性 b 會(huì)靜默失敗。在嚴(yán)格模式下率寡,將會(huì)拋出 TypeError 錯(cuò)誤迫卢。

3. 密封

Object.seal(..) 會(huì)創(chuàng)建一個(gè)“密封”的對(duì)象,這個(gè)方法實(shí)際上會(huì)在一個(gè)現(xiàn)有對(duì)象上調(diào)用 Object.preventExtensions(..) 并把所有現(xiàn)有屬性標(biāo)記為 configurable:false勇劣。
所以靖避,密封之后不僅不能添加新屬性,也不能重新配置或者刪除任何現(xiàn)有屬性(雖然可以修改屬性的值)比默。

4. 凍結(jié)

Object.freeze(..) 會(huì)創(chuàng)建一個(gè)凍結(jié)對(duì)象幻捏,這個(gè)方法實(shí)際上會(huì)在一個(gè)現(xiàn)有對(duì)象上調(diào)用 Object.seal(..) 并把所有“數(shù)據(jù)訪(fǎng)問(wèn)”屬性標(biāo)記為 writable:false,這樣就無(wú)法修改它們的值命咐。
這個(gè)方法是你可以應(yīng)用在對(duì)象上的級(jí)別最高的不可變性篡九,它會(huì)禁止對(duì)于對(duì)象本身及其任意直接屬性的修改(不過(guò)就像我們之前說(shuō)過(guò)的,這個(gè)對(duì)象引用的其他對(duì)象是不受影響的)醋奠。
你可以“深度凍結(jié)”一個(gè)對(duì)象榛臼,具體方法為,首先在這個(gè)對(duì)象上調(diào)用 Object.freeze(..)窜司, 然后遍歷它引用的所有對(duì)象并在這些對(duì)象上調(diào)用 Object.freeze(..)沛善。但是一定要小心,因 為這樣做有可能會(huì)在無(wú)意中凍結(jié)其他(共享)對(duì)象塞祈。


3.3.7 [[Get]]

在語(yǔ)言規(guī)范中金刁,myObject.amyObject 上實(shí)際上是實(shí)現(xiàn)了 [[Get]] 操作(有點(diǎn)像函數(shù)調(diào) 用:[[Get]]())。對(duì)象默認(rèn)的內(nèi)置 [[Get]] 操作首先在對(duì)象中查找是否有名稱(chēng)相同的屬性议薪, 如果找到就會(huì)返回這個(gè)屬性的值尤蛮。
然而,如果沒(méi)有找到名稱(chēng)相同的屬性斯议,按照 [[Get]] 算法的定義會(huì)執(zhí)行另外一種非常重要的行為遍歷可能存在的 [[Prototype]] 鏈产捞, 也就是原型鏈
如果無(wú)論如何都沒(méi)有找到名稱(chēng)相同的屬性哼御,那 [[Get]] 操作會(huì)返回值 undefined


3.3.8 [[Put]]

[[Put]] 被觸發(fā)時(shí)坯临,實(shí)際的行為取決于許多因素,包括對(duì)象中是否已經(jīng)存在這個(gè)屬性(這是最重要的因素)艇搀。
如果已經(jīng)存在這個(gè)屬性尿扯,[[Put]] 算法大致會(huì)檢查下面這些內(nèi)容。

  1. 屬性是否是訪(fǎng)問(wèn)描述符(參見(jiàn)3.3.9節(jié))?如果是并且存在setter就調(diào)用setter焰雕。
  2. 屬性的數(shù)據(jù)描述符中writable是否是false?如果是,在非嚴(yán)格模式下靜默失敗芳杏,在嚴(yán)格模式下拋出 TypeError 異常矩屁。
  3. 如果都不是辟宗,將該值設(shè)置為屬性的值。

如果對(duì)象中不存在這個(gè)屬性吝秕,[[Put]] 操作會(huì)更加復(fù)雜泊脐。我們會(huì)在第 5 章討論 [[Prototype]] 時(shí)詳細(xì)進(jìn)行介紹。


3.3.9 Getter和Setter

對(duì)象默認(rèn)的 [[Put]] 和 [[Get]] 操作分別可以控制屬性值的設(shè)置和獲取烁峭。
在 ES5 中可以使用 getter 和 setter 部分改寫(xiě)默認(rèn)操作容客,但是只能應(yīng)用在單個(gè)屬性上,無(wú)法 應(yīng)用在整個(gè)對(duì)象上约郁。getter 是一個(gè)隱藏函數(shù)缩挑,會(huì)在獲取屬性值時(shí)調(diào)用。setter 也是一個(gè)隱藏 函數(shù)鬓梅,會(huì)在設(shè)置屬性值時(shí)調(diào)用供置。
當(dāng)你給一個(gè)屬性定義 getter、setter 或者兩者都有時(shí)绽快,這個(gè)屬性會(huì)被定義為“訪(fǎng)問(wèn)描述符”(和“數(shù)據(jù)描述符”相對(duì))芥丧。對(duì)于訪(fǎng)問(wèn)描述符來(lái)說(shuō),JavaScript 會(huì)忽略它們的 value 和 writable 特性坊罢,取而代之的是關(guān)心 set 和 get(還有 configurable 和 enumerable)特性续担。

var myObject = {
    // 給 a 定義一個(gè) getter 
    get a() {
        return 2; 
    }
};
Object.defineProperty( 
    myObject, // 目標(biāo)對(duì)象 
    "b", // 屬性名
    {   // 描述符
        // 給 b 設(shè)置一個(gè) getter
        get: function(){ 
            return this.a * 2 
        },
        // 確保 b 會(huì)出現(xiàn)在對(duì)象的屬性列表中
        enumerable: true
    }
);
myObject.a; // 2
myObject.b; // 4

不管是對(duì)象文字語(yǔ)法中的get a(){..},還是 defineProperty(..) 中的顯式定義活孩,二者都會(huì)在對(duì)象中創(chuàng)建一個(gè)不包含值的屬性物遇,對(duì)于這個(gè)屬性的訪(fǎng)問(wèn)會(huì)自動(dòng)調(diào)用一個(gè)隱藏函數(shù),它的返回值會(huì)被當(dāng)作屬性訪(fǎng)問(wèn)的返回值诱鞠。
為了讓屬性更合理挎挖,還應(yīng)當(dāng)定義 setter,和你期望的一樣航夺,setter 會(huì)覆蓋單個(gè)屬性默認(rèn)的 [Put]操作蕉朵。通常來(lái)說(shuō) getter 和 setter 是成對(duì)出現(xiàn)的(只定義一個(gè)的話(huà) 通常會(huì)產(chǎn)生意料之外的行為):

var myObject = {
    // 給 a 定義一個(gè) getter 
    get a() {
        return this._a_; 
    },
    // 給 a 定義一個(gè) setter 
    set a(val) {
        this._a_ = val * 2; 
    }
};
myObject.a = 2;
myObject.a; // 4


3.3.10 存在性
  • in 操作符會(huì)檢查屬性是否在對(duì)象及其 [[Prototype]] 原型鏈中
  • hasOwnProperty(..) 只會(huì)檢查屬性是否在 myObject 對(duì)象中,不會(huì)檢查 [[Prototype]] 鏈
1. 枚舉
  • for..in 循環(huán)可以用來(lái)遍歷對(duì)象的可枚舉屬性列表(包括 [[Prototype]] 鏈)阳掐。
  • propertyIsEnumerable(..) 會(huì)檢查給定的屬性名是否直接存在于對(duì)象中(而不是在原型鏈 上)并且滿(mǎn)足 enumerable:true始衅。
  • Object.keys(..) 會(huì)返回一個(gè)數(shù)組,包含所有可枚舉屬性缭保,只會(huì)查找對(duì)象直接包含的屬性汛闸。
  • Object.getOwnPropertyNames(..) 會(huì)返回一個(gè)數(shù)組,包含所有屬性艺骂,無(wú)論它們是否可枚舉诸老,只會(huì)查找對(duì)象直接包含的屬性。
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"]


3.4 遍歷

  • for..in 循環(huán)可以用來(lái)遍歷對(duì)象的可枚舉屬性列表(包括 [[Prototype]] 鏈)钳恕。
  • forEach(..)别伏、every(..) 蹄衷、some(..)
  • for..of,循環(huán)每次調(diào)用 myObject 迭代器對(duì)象的 next() 方法時(shí)厘肮,內(nèi)部的指針都會(huì)向前移動(dòng)并 返回對(duì)象屬性列表的下一個(gè)值愧口。
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();   // 使用 ES6 中的符號(hào) Symbol.iterator 來(lái)獲取對(duì)象的 @@iterator 內(nèi)部屬 性。
it.next(); // { value:1, done:false } 
it.next(); // { value:2, done:false } 
it.next(); // { value:3, done:false } 
it.next(); // { done:true }

注:和數(shù)組不同类茂,普通的對(duì)象沒(méi)有內(nèi)置的 @@iterator耍属,所以無(wú)法自動(dòng)完成 for..of 遍歷。


第4章 混合對(duì)象“類(lèi)”

類(lèi)巩检、繼承厚骗、實(shí)例化、多態(tài)

4.1.1 “類(lèi)”設(shè)計(jì)模式
  • 面向?qū)ο笤O(shè)計(jì)模式碴巾,比如迭代器模式溯捆、觀察者模式、工廠(chǎng)模式厦瓢、單例模式提揍,等等。
  • 最好使用類(lèi)把過(guò)程化風(fēng)格的“意大利面代碼”轉(zhuǎn)換成結(jié)構(gòu)清晰煮仇、組織良好的代碼劳跃。
4.2 類(lèi)的機(jī)制
  • 一個(gè)類(lèi)就是一張藍(lán)圖。為了獲得真正可以交互的對(duì)象浙垫,我們必須按照類(lèi)來(lái)建造(也可以說(shuō)實(shí)例化)一個(gè)東西刨仑,這個(gè)東西通常被稱(chēng)為實(shí)例。這個(gè)對(duì)象就是類(lèi)中描述的所有特性的一份副本夹姥。
4.3.1 多態(tài)

多態(tài)是說(shuō)父類(lèi)的通用行為可以被子類(lèi)用更特殊的行為重寫(xiě)杉武。
多態(tài)并不表示子類(lèi)和父類(lèi)有關(guān)聯(lián),子類(lèi)得到的只是父類(lèi)的一份副本辙售。類(lèi)的繼承其實(shí)就是復(fù)制轻抱。


第5章 原型

5.1 [[Prototype]]
  • 幾乎所有的對(duì)象在創(chuàng)建時(shí) [[Prototype]] 屬性都會(huì)被賦予一個(gè)非空的值。
  • 對(duì)于默認(rèn)的 [[Get]] 操作來(lái)說(shuō)旦部,如果無(wú)法在對(duì)象本身找到需要的屬性祈搜,就會(huì)繼續(xù)訪(fǎng)問(wèn)對(duì)象的 [[Prototype]] 鏈。這個(gè)過(guò)程會(huì)持續(xù)到找到匹配的屬性名或者查找完整條 [[Prototype]] 鏈士八。如果是后者的話(huà)容燕, [[Get]] 操作的返回值是 undefined
  • 使用 for..in 遍歷對(duì)象時(shí)婚度,使用 in 操作符來(lái)檢查屬性在對(duì)象中是否存在時(shí)蘸秘,同樣會(huì)查找對(duì)象的整條原型鏈
5.1.1 [[Prototype]] 的“盡頭”

所有普通的 [[Prototype]] 鏈最終都會(huì)指向內(nèi)置的 Object.prototype,所以它包含 JavaScript 中許多通用的功能。

5.1.2 屬性設(shè)置和屏蔽

如果 foo 不直接存在于 myObject 中而是存在于原型鏈上層時(shí) myObject.foo = "bar" 會(huì)出現(xiàn)的三種情況秘血。

  1. 如果在[[Prototype]]鏈上層存在名為foo的普通數(shù)據(jù)訪(fǎng)問(wèn)屬性(參見(jiàn)第3章)并且沒(méi) 有被標(biāo)記為只讀(writable:false)味抖,那就會(huì)直接在 myObject 中添加一個(gè)名為 foo 的新 屬性评甜,它是屏蔽屬性灰粮。
  2. 如果在[[Prototype]]鏈上層存在foo,但是它被標(biāo)記為只讀(writable:false)忍坷,那么 無(wú)法修改已有屬性或者在 myObject 上創(chuàng)建屏蔽屬性粘舟。如果運(yùn)行在嚴(yán)格模式下,代碼會(huì) 拋出一個(gè)錯(cuò)誤佩研。否則柑肴,這條賦值語(yǔ)句會(huì)被忽略⊙恚總之晰骑,不會(huì)發(fā)生屏蔽。
  3. 如果在[[Prototype]]鏈上層存在foo并且它是一個(gè)setter(參見(jiàn)第3章)绊序,那就一定會(huì) 調(diào)用這個(gè) setter硕舆。foo 不會(huì)被添加到(或者說(shuō)屏蔽于)myObject,也不會(huì)重新定義 foo 這 個(gè) setter骤公。
    有些情況下會(huì)隱式產(chǎn)生屏蔽抚官,一定要當(dāng)心。思考下面的代碼:
var anotherObject = { 
    a:2
};

var myObject = Object.create( anotherObject );

anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false

myObject.a++; // 隱式屏蔽! 

anotherObject.a; // 2
myObject.a; // 3

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

盡管 myObject.a++ 看起來(lái)應(yīng)該(通過(guò)委托)查找并增加 anotherObject.a 屬性阶捆,但是別忘了 ++ 操作相當(dāng)于 myObject.a = myObject.a + 1凌节。因此 ++ 操作首先會(huì)通過(guò) [[Prototype]] 查找屬性 a 并從 anotherObject.a 獲取當(dāng)前屬性值 2,然后給這個(gè)值加 1洒试,接著用 [[Put]] 將值 3 賦給 myObject 中新建的屏蔽屬性 a倍奢,天吶!
修改委托屬性時(shí)一定要小心。如果想讓 anotherObject.a 的值增加垒棋,唯一的辦法是 anotherObject.a++卒煞。


5.2 “類(lèi)”

5.2.1 “類(lèi)”函數(shù)

所有的函數(shù)默認(rèn)都會(huì)擁有一個(gè)名為 prototype 的公有并且不可枚舉的屬性,它會(huì)指向另一個(gè)對(duì)象:

function Foo() { 
    // ...
}
Foo.prototype; // { }

var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true

new Foo() 只是間接完成我們的目標(biāo):一個(gè)關(guān)聯(lián)到其他對(duì)象的新對(duì)象捕犬。

5.2.2 “構(gòu)造函數(shù)”
function Foo() { 
    // ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
// a.constructor 只是通過(guò)默認(rèn)的 [[Prototype]] 委托指向 Foo

Foo.prototype 默認(rèn)有一個(gè)公有并且不可枚舉的屬性 .constructor跷坝,這個(gè)屬性引用的是對(duì)象關(guān)聯(lián)的函數(shù)(本例中是 Foo)。此外碉碉,我們可以看到通過(guò)“構(gòu)造函數(shù)”調(diào)用 new Foo() 創(chuàng)建的對(duì)象也有一個(gè) .constructor 屬性柴钻,指向 “創(chuàng)建這個(gè)對(duì)象的函數(shù)”。
實(shí)際上 a 本身并沒(méi)有 .constructor 屬性垢粮。而且贴届,雖然 a.constructor 確實(shí)指向 Foo 函數(shù),但是這個(gè)屬性并不是表示 a 由 Foo“構(gòu)造”。實(shí)際上毫蚓,.constructor 引用同樣被委托給了 Foo.prototype占键,而 Foo.prototype.constructor 默認(rèn)指向 Foo。a.constructor 只是通過(guò)默認(rèn)的 [[Prototype]] 委托指向 Foo元潘,這和“構(gòu)造”毫無(wú)關(guān)系畔乙。

5.3 (原型)繼承

  • 下面這段代碼使用的就是典型的“原型風(fēng)格”:
function Foo(name) { 
    this.name = name;
}
Foo.prototype.myName = function() { 
    return this.name;
};
function Bar(name,label) { 
    Foo.call( this, name ); 
    this.label = label;
}
// 我們創(chuàng)建了一個(gè)新的 Bar.prototype 對(duì)象并關(guān)聯(lián)到 Foo.prototype 
Bar.prototype = Object.create( Foo.prototype );
// 注意!現(xiàn)在沒(méi)有 Bar.prototype.constructor 了 
// 如果你需要這個(gè)屬性的話(huà)可能需要手動(dòng)修復(fù)一下它
Bar.prototype.myLabel = function() { 
    return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"

這段代碼的核心部分就是語(yǔ)句 Bar.prototype = Object.create( Foo.prototype )。調(diào)用 Object.create(..) 會(huì)憑空創(chuàng)建一個(gè)“新”對(duì)象并把新對(duì)象內(nèi)部的 [[Prototype]] 關(guān)聯(lián)到你指定的對(duì)象(本例中是 Foo.prototype)翩概。

  • ES6 添加了輔助函數(shù) Object.setPrototypeOf(..)牲距,可以用標(biāo)準(zhǔn)并且可靠的方法來(lái)修改對(duì)象的 [[Prototype]] 關(guān)聯(lián)。
    我們來(lái)對(duì)比一下兩種把 Bar.prototype 關(guān)聯(lián)到 Foo.prototype 的方法:
// ES6 之前需要拋棄默認(rèn)的 Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );
// ES6 開(kāi)始可以直接修改現(xiàn)有的 Bar.prototype 
Object.setPrototypeOf( Bar.prototype, Foo.prototype );

如果忽略掉 Object.create(..) 方法帶來(lái)的輕微性能損失(拋棄的對(duì)象需要進(jìn)行垃圾回 收)钥庇,它實(shí)際上比 ES6 及其之后的方法更短而且可讀性更高牍鞠。不過(guò)無(wú)論如何,這是兩種完全不同的語(yǔ)法评姨。

檢查“類(lèi)”關(guān)系

思考下面的代碼:

function Foo() { 
    // ...
}
Foo.prototype.blah = ...; 
var a = new Foo();


  • 第一種方法是站在“類(lèi)”的角度來(lái)判斷:
a instanceof Foo; // true

instanceof 回答的問(wèn)題是:在 a 的整條 [[Prototype]] 鏈中是否有指向 Foo.prototype 的對(duì)象?
instanceof 操作符的左操作數(shù)是一個(gè)普通的對(duì)象难述,右操作數(shù)是一個(gè)函數(shù)。
可惜吐句,這個(gè)方法只能處理對(duì)象(a)和函數(shù)(帶 .prototype 引用的 Foo)之間的關(guān)系胁后。如 果你想判斷兩個(gè)對(duì)象(比如 a 和 b)之間是否通過(guò) [[Prototype]] 鏈關(guān)聯(lián),只用 instanceof 無(wú)法實(shí)現(xiàn)蕴侧。

  • 第二種判斷 [[Prototype]] 反射的方法:
Foo.prototype.isPrototypeOf( a ); // true

isPrototypeOf(..) 回答的問(wèn)題是:在 a 的整條 [[Prototype]] 鏈中是否出現(xiàn)過(guò)Foo.prototype ?

  • 我們也可以直接獲取一個(gè)對(duì)象的 [[Prototype]] 鏈择同。在 ES5 中,標(biāo)準(zhǔn)的方法是: Object.getPrototypeOf( a )
    可以驗(yàn)證一下净宵,這個(gè)對(duì)象引用是否和我們想的一樣:
Object.getPrototypeOf( a ) === Foo.prototype; // true
  • 絕大多數(shù)(不是所有!)瀏覽器也支持一種非標(biāo)準(zhǔn)的方法來(lái)訪(fǎng)問(wèn)內(nèi)部 [[Prototype]] 屬性:__proto__
a.__proto__ === Foo.prototype; // true

.proto 看起來(lái)很像一個(gè)屬性敲才,但是實(shí)際上它更像一個(gè) getter/setter:

Object.defineProperty( Object.prototype, "__proto__", { 
    get: function() {
        return Object.getPrototypeOf( this ); },
    set: function(o) {
        // ES6 中的 setPrototypeOf(..) 
        Object.setPrototypeOf( this, o ); 
        return o;
    } 
} );


5.4 對(duì)象關(guān)聯(lián)

[[Prototype]] 機(jī)制就是存在于對(duì)象中的一個(gè)內(nèi)部鏈接,它會(huì)引用其他
對(duì)象择葡。
通常來(lái)說(shuō)紧武,這個(gè)鏈接的作用是:如果在對(duì)象上沒(méi)有找到需要的屬性或者方法引用,引擎就 會(huì)繼續(xù)在 [[Prototype]] 關(guān)聯(lián)的對(duì)象上進(jìn)行查找敏储。同理阻星,如果在后者中也沒(méi)有找到需要的 引用就會(huì)繼續(xù)查找它的 [[Prototype]],以此類(lèi)推已添。這一系列對(duì)象的鏈接被稱(chēng)為“原型鏈”妥箕。

5.4.1 創(chuàng)建關(guān)聯(lián)
Object.create(..)
  • Object.create(..) 會(huì)創(chuàng)建一個(gè)新對(duì)象(bar)并把它關(guān)聯(lián)到我們指定的對(duì)象(foo),這樣 我們就可以充分發(fā)揮 [[Prototype]] 機(jī)制的威力(委托)并且避免不必要的麻煩(比如使用 new 的構(gòu)造函數(shù)調(diào)用會(huì)生成 .prototype 和 .constructor 引用)更舞。
  • Object.create(null) 會(huì) 創(chuàng) 建 一 個(gè) 擁 有 空( 或 者 說(shuō) null)[[Prototype]] 鏈接的對(duì)象畦幢,這個(gè)對(duì)象無(wú)法進(jìn)行委托。由于這個(gè)對(duì)象沒(méi)有原型鏈缆蝉,所以 instanceof 操作符(之前解釋過(guò))無(wú)法進(jìn)行判斷宇葱,因此總是會(huì)返回 false瘦真。
  • Object.create()的polyfill代碼(兼容舊IE)
if (!Object.create) { 
    Object.create = function(o) {
    function F(){} 
    F.prototype = o; 
    return new F();
}; }
  • Object.create()的擴(kuò)展
    Object.create(..) 的第二個(gè)參數(shù)指定了需要添加到新對(duì)象中的屬性名以及這些屬性的屬性描述符:
var anotherObject = { 
    a:2
};
var myObject = Object.create( anotherObject, { 
    b: {
        enumerable: false, 
        writable: true, 
        configurable: false, 
        value: 3
    }, 
    c: {
        enumerable: true, 
        writable: false, 
        configurable: false, 
        value: 4
    } 
});

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

myObject.a; // 2
myObject.b; // 3
myObject.c; // 4

5.5 小結(jié)

如果要訪(fǎng)問(wèn)對(duì)象中并不存在的一個(gè)屬性,[[Get]] 操作就會(huì)查找對(duì)象內(nèi)部[[Prototype]] 關(guān)聯(lián)的對(duì)象黍瞧。這個(gè)關(guān)聯(lián)關(guān)系實(shí)際上定義了一條“原型鏈”(有點(diǎn)像嵌套的作用域鏈)诸尽,在查找屬性時(shí)會(huì)對(duì)它進(jìn)行遍歷。
所有普通對(duì)象都有內(nèi)置的 Object.prototype印颤,指向原型鏈的頂端(比如說(shuō)全局作用域)您机,如 果在原型鏈中找不到指定的屬性就會(huì)停止。toString()膀哲、valueOf() 和其他一些通用的功能 都存在于 Object.prototype 對(duì)象上往产,因此語(yǔ)言中所有的對(duì)象都可以使用它們。
關(guān)聯(lián)兩個(gè)對(duì)象最常用的方法是使用 new 關(guān)鍵詞進(jìn)行函數(shù)調(diào)用某宪,在調(diào)用的 4 個(gè)步驟(第 2 章)中會(huì)創(chuàng)建一個(gè)關(guān)聯(lián)其他對(duì)象的新對(duì)象。
使用 new 調(diào)用函數(shù)時(shí)會(huì)把新對(duì)象的 .prototype 屬性關(guān)聯(lián)到“其他對(duì)象”锐朴。帶 new 的函數(shù)調(diào)用 通常被稱(chēng)為“構(gòu)造函數(shù)調(diào)用”兴喂,盡管它們實(shí)際上和傳統(tǒng)面向類(lèi)語(yǔ)言中的類(lèi)構(gòu)造函數(shù)不一樣。
雖然這些 JavaScript 機(jī)制和傳統(tǒng)面向類(lèi)語(yǔ)言中的“類(lèi)初始化”和“類(lèi)繼承”很相似焚志,但 是 JavaScript 中的機(jī)制有一個(gè)核心區(qū)別衣迷,那就是不會(huì)進(jìn)行復(fù)制,對(duì)象之間是通過(guò)內(nèi)部的 [[Prototype]] 鏈關(guān)聯(lián)的酱酬。
出于各種原因壶谒,以“繼承”結(jié)尾的術(shù)語(yǔ)(包括“原型繼承”)和其他面向?qū)ο蟮男g(shù)語(yǔ)都無(wú) 法幫助你理解 JavaScript 的真實(shí)機(jī)制(不僅僅是限制我們的思維模式)。
相比之下膳沽,“委托”是一個(gè)更合適的術(shù)語(yǔ)汗菜,因?yàn)閷?duì)象之間的關(guān)系不是復(fù)制而是委托。

第6章 行為委托

類(lèi)模型(面向?qū)ο箫L(fēng)格)
function Foo(who) { 
    this.me = who;
}
Foo.prototype.identify = function() {
    return "I am " + this.me; 
};
function Bar(who) { 
    Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
    alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" ); 
b1.speak();
b2.speak();
委托模型(對(duì)象關(guān)聯(lián)風(fēng)格)
Foo = {
    init: function(who) {
        this.me = who; 
    },
    identify: function() {
        return "I am " + this.me;
    } 
};
Bar = Object.create( Foo );
Bar.speak = function() {
    alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();
6.6 小結(jié)

在軟件架構(gòu)中你可以選擇是否使用類(lèi)和繼承設(shè)計(jì)模式挑社。大多數(shù)開(kāi)發(fā)者理所當(dāng)然地認(rèn)為類(lèi)是 唯一(合適)的代碼組織方式陨界,但是本章中我們看到了另一種更少見(jiàn)但是更強(qiáng)大的設(shè)計(jì)模式:行為委托。
行為委托認(rèn)為對(duì)象之間是兄弟關(guān)系痛阻,互相委托菌瘪,而不是父類(lèi)和子類(lèi)的關(guān)系。JavaScript 的 [[Prototype]] 機(jī)制本質(zhì)上就是行為委托機(jī)制阱当。也就是說(shuō)俏扩,我們可以選擇在 JavaScript 中努 力實(shí)現(xiàn)類(lèi)機(jī)制(參見(jiàn)第 4 和第 5 章),也可以擁抱更自然的 [[Prototype]] 委托機(jī)制弊添。
當(dāng)你只用對(duì)象來(lái)設(shè)計(jì)代碼時(shí)录淡,不僅可以讓語(yǔ)法更加簡(jiǎn)潔,而且可以讓代碼結(jié)構(gòu)更加清晰表箭。 對(duì)象關(guān)聯(lián)(對(duì)象之前互相關(guān)聯(lián))是一種編碼風(fēng)格赁咙,它倡導(dǎo)的是直接創(chuàng)建和關(guān)聯(lián)對(duì)象钮莲,不把它們抽象成類(lèi)。對(duì)象關(guān)聯(lián)可以用基于 [[Prototype]] 的行為委托非常自然地實(shí)現(xiàn)彼水。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末崔拥,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子凤覆,更是在濱河造成了極大的恐慌链瓦,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盯桦,死亡現(xiàn)場(chǎng)離奇詭異慈俯,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)拥峦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)贴膘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人略号,你說(shuō)我怎么就攤上這事刑峡。” “怎么了玄柠?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵突梦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我羽利,道長(zhǎng)宫患,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任这弧,我火速辦了婚禮娃闲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘当宴。我一直安慰自己畜吊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布户矢。 她就那樣靜靜地躺著玲献,像睡著了一般。 火紅的嫁衣襯著肌膚如雪梯浪。 梳的紋絲不亂的頭發(fā)上捌年,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音挂洛,去河邊找鬼礼预。 笑死,一個(gè)胖子當(dāng)著我的面吹牛虏劲,可吹牛的內(nèi)容都是我干的托酸。 我是一名探鬼主播褒颈,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼励堡!你這毒婦竟也來(lái)了谷丸?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤应结,失蹤者是張志新(化名)和其女友劉穎刨疼,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鹅龄,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡揩慕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扮休。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片迎卤。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖肛炮,靈堂內(nèi)的尸體忽然破棺而出止吐,到底是詐尸還是另有隱情,我是刑警寧澤侨糟,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站瘩燥,受9級(jí)特大地震影響秕重,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜厉膀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一溶耘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧服鹅,春花似錦凳兵、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至仗哨,卻和暖如春形庭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厌漂。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工萨醒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人苇倡。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓富纸,卻偏偏與公主長(zhǎng)得像囤踩,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子晓褪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

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