深入理解JavaScript的面向?qū)ο髾C制和原型鏈

  • 〇、前言
  • 一盒至、JavaScript和Java在面向?qū)ο髾C制上的區(qū)別
    • 1酗洒、面向?qū)ο缶幊痰奶卣?/li>
    • 2、機制差異簡述
  • 二枷遂、面向?qū)ο髾C制差異舉例
    • 1樱衷、Java版的繼承機制例子
    • 2、JavaScript版的派生例子
  • 三酒唉、JavaScript的面向?qū)ο蟮幕A(chǔ)知識點
  • 四矩桂、結(jié)合代碼和對象圖解讀理解原型鏈
  • 五、總結(jié)

〇痪伦、前言

JavaScript是一種面向?qū)ο蟮恼Z言侄榴,但它并沒有采用Java那種基于類(class-based)的方式來實現(xiàn),而是采用基于原型(prototype-based)方式----這種方式能提供更靈活的機制网沾,但沒有基于類方式那么簡潔癞蚕,因而苦澀難懂。

本文嘗試通過一段例子代碼和其對應(yīng)的UML對象圖來闡述JavaScript的面向?qū)ο髾C制辉哥,以讓讀者能快速桦山、深入地理解。

本文前面部分有一些理論知識醋旦,略顯枯燥恒水,你可以先瀏覽一下文中的圖、代碼例子饲齐,以便先有個感性認識寇窑,這里先提前貼一下下文中會出現(xiàn)的圖:

注1:本文假定讀者有一定的JavaScript基礎(chǔ),以及對Java的語法有初步的了解箩张。

注2:ES6(ES2015)引入了class甩骏,但只是語法糖(Syntactic sugar),
  所以還是有必要理解JavaScript的基于原型的機制先慷。

注3:本文有時會用JS代替JavaScript饮笛,OO代替Object Oriented(面向?qū)ο螅?

一、JavaScript和Java在面向?qū)ο髾C制上的區(qū)別

ES6(ES2015)引入了class论熙,但只是語法糖(Syntactic sugar)福青,即是說,核心機制還是基于原型的脓诡,所以還是有必要理解JavaScript的基于原型的OO機制无午。

下文主要以ES5為基準,所以以“JS中沒有class這樣東西”為論調(diào)祝谚。

一.1宪迟、面向?qū)ο缶幊痰奶卣?/h3>

JS也是一門面向?qū)ο缶幊?/code>的語言,所以也具備這有3個OO特征交惯,這里簡單概述一下次泽。(跟本文關(guān)系最密切的是繼承Inheritance)

  1. 封裝 Encapsulation:把跟一種對象(一個class)相關(guān)的屬性、操作放在一起席爽,便于理解意荤、編程,同時可以指定這些屬性只锻、操作的訪問權(quán)限玖像;
  2. 繼承 Inheritance:允許一個類繼承另外一個類的屬性、操作齐饮,必要時改寫這些屬性和行為----被繼承的類被稱之為父類(superclass)捐寥,繼承父類的類被稱為子類(subclass);
  3. 多態(tài) Polymorphism:允許在某個API或某段代碼中指定使用某個類的instances沈矿,而在運行態(tài)時上真,可以傳遞這個類的子類的instances給這個API或這段代碼。

一.2羹膳、機制差異簡述

要快速睡互、準確地理解原型鏈,最好的辦法是通過Java的OO機制對比著來理解陵像。兩者主要差別有:

  1. Java的OO機制是基于類(class-based)的,而JS是基于原型(prototype-based)的醒颖;
  2. 具體來說妻怎,就是Java中分別有(class)和實例(instance)的區(qū)分泞歉;而JS中沒有class這個概念匿辩,與class類似的是prototype(原型),而與instance類似的是object(對象)铲球;
  3. Java通過class與class之間的繼承來實現(xiàn)繼承(Inheritance),比較簡潔晰赞、直觀;而JS是通過構(gòu)造函數(shù)(constructor)和其對應(yīng)的原型(prototype)掖鱼,以及原型鏈(prototype chain)來實現(xiàn)的,比較復雜戏挡。(下文有圖為證)

二、面向?qū)ο髾C制差異舉例

這部分先給出Java的例子增拥,然后用JS實現(xiàn)同樣的功能啄巧,以此展示JS的OO機制的特點掌栅、復雜性秩仆。

Java的OO機制在語法上太簡單了,以至于哪怕你不懂Java猾封,但只要你對JavaScript的OO機制有初步的了解澄耍,也能讀懂下面的Java例子。

二.1晌缘、Java版的繼承機制例子

直接上代碼吧齐莲,請留意代碼中的注釋,以及注釋中通過“->”標識的輸出結(jié)果磷箕。

class InheritanceDemo {
    class Clock {
        protected String mTimezone; // 實例屬性
        
        public Clock(String timezone) { // 構(gòu)造函數(shù)
            mTimezone = timezone;
        }
        
        public void greeting() { // 實例方法
            System.out.println("A clock - " + mTimezone);
        }
    }
    
    class PrettyClock extends Clock { // 類的派生
        protected String mColor;
        
        public PrettyClock(String timezone, String color) {
            super(timezone); // 調(diào)用父類的構(gòu)造函數(shù)
            
            mColor = color;
        }
        
        @Override
        public void greeting() { // 重寫(override)父類的一個方法
            System.out.println("A clock - " + mTimezone + " + "+ mColor);
        }
        
    }
    
    public void demo() {
        Clock clock1 = new Clock("London");
        Clock clock2 = new Clock("Shanghai");
        Clock clock3 = new PrettyClock("Chongqing", "RED");
        
        clock1.greeting(); // -> "A clock - London"
        clock2.greeting(); // -> "A clock - Shanghai"
        clock3.greeting(); // -> "A clock - Chongqing + RED"
    }
    
    public static void main(String[] args) {
        (new InheritanceDemo()).demo();
    }
}

上述的Clock和PrettyClock的繼承關(guān)系选酗,用UML類圖方式表達如下:

Java繼承的UML類圖表達

注:Clock類沒有指明父類,默認派生于Object這個class岳枷。

二.2芒填、JavaScript版的派生例子

在ES6/ES2015添加class這個語法糖之前,要實現(xiàn)繼承則比較復雜空繁,下面是其中一種方法(這段JS代碼實現(xiàn)跟上面的Java代碼段一樣的功能).

注:這段代碼在Node.js或者Chrome的console中運行通過殿衰。

function Clock(timezone) { // 構(gòu)造函數(shù)
    this.timezone = timezone; // 添加實例屬性
}

// JS默認就為構(gòu)造函數(shù)添加了一個prototype屬性

// 為Clock類(通過其prototype屬性)添加實例方法
Clock.prototype.greeting = function() {
    console.log("A clock - " + this.timezone);
}

// 定義子類 - 此為子類的構(gòu)造函數(shù)
function PrettyClock(timezone, color) {
    Clock.call(this, timezone); // 調(diào)用父類的constructor
    
    this.color = color; // 添加實例屬性
}

// 通過操作子類構(gòu)造函數(shù)的prototype來使其成為子類
PrettyClock.prototype = Object.create(Clock.prototype);
PrettyClock.prototype.constructor = PrettyClock; // 手工添加
PrettyClock.prototype.greeting = function() {
    console.log("A clock - " + this.timezone + " + " + this.color);
}

function demo() {
    var clock1 = new Clock("London");
    var clock2 = new Clock("Shanghai");
    var clock3 = new PrettyClock("Chongqing", "RED");

    clock1.greeting();  // -> "A clock - London"
    clock2.greeting();  // -> "A clock - Shanghai"
    clock3.greeting();  // -> "A clock - Chongqing + RED"
}

demo();

對比Java和JS兩個版本的繼承例子代碼,可以看出JS的OO語法遠沒有Java的那么簡潔盛泡、直觀 ---- 這點差異其實不算什么了闷祥,難點在于后面要介紹的原型鏈機制,這難點可以在這個例子中的JS的OO機制(原型鏈)的對象圖(object diagram)中看出來:

JavaScript原型鏈

一時看不懂這張圖傲诵?沒關(guān)系凯砍,因為不是理解能力的問題箱硕,而是JS的OO機制實在太不直觀了。下面需要先補充一些JS知識果覆,然后解釋這幅圖颅痊。

三、JavaScript的面向?qū)ο蟮幕A(chǔ)知識點

這里的幾點知識點很重要局待,建議先好好理解一下,必要時多看幾遍再讀下一部分菱属。

因為它們之間關(guān)系比較緊密,所以就按這種方式來排版了纽门,請理解一下。

  1. JS中赏陵,對象(object)是指一組屬性(property,每個屬性都是一對key-value)的集合蝙搔,例如{x: 11, y: 22};實際上吃型,
    • 一個函數(shù)也是一個object,所以
      • (function() {}) instanceof Object // -> true勤晚;
    • 連Object、Function赐写、String等這些關(guān)鍵字對應(yīng)的東西都是objects:
      • Object instanceof Object // -> true
      • Function instanceof Object // -> true
      • String instanceof Object // -> true
    • 除了Boolean, Number, String, Null, Undefined, Symbol(ES6/ES2015中加入)類型的值外,其它的都是Object類型的值(即都是objects)挺邀;所以:
      • 123 instanceof Object // -> false -- 數(shù)值123是原始值(Primitive value)而不是對象揉忘;
      • "hello" instanceof Object // -> false -- 字符串也是原始值(Primitive value)癌淮;
      • "hello".length // -> 5 -- 字符串用起來像對象,原因是JS隱性地使用了包裹對象(Wrapper objects乳蓄,詳情)來封裝夕膀;
      • "hello".toUpperCase() // -> "HELLO" -- 同上美侦;
  2. JS的objects可以被看作是Java中的Map的instances魂奥,并且JS支持用對象直接量(Object literal)來創(chuàng)建對象。這一點需要記住耻煤,因為下文很多例子用到這個語法:
    • var o1 = {}; o1.age = 18; - 創(chuàng)建一個object,并添加一個名為age的屬性哈蝇;
    • var o2 = {name: "Alan", age: 18} - 創(chuàng)建了一個object,里面有兩個屬性怜跑;
    • ({name:"Alan", age:18}).age === 18 // -> true - 創(chuàng)建了一個object并訪問其中一個屬性;
  3. JS的OO機制中沒有Java的class這東西(ES6/ES2015的class只是語法糖)性芬,JS的OO機制是通過原型(Prototype)來實現(xiàn)的剧防,而你看到的Object, Function, String等,其實并不是class(雖然它們是大寫字母開頭的诵姜,而Java要求類名以大寫字母開頭),而是構(gòu)造函數(shù)(Constructor)棚唆,因為函數(shù)也是對象;
    • typeof Object // -> "function"
    • typeof String // -> "function"
  4. JS為每個function都默認添加了一個prototype屬性鞋囊;
    • 這個屬性的值是個對象:
      • typeof (function() {}).prototype // -> "object"
    • 默認情況下,每個function的prototype值都是不一樣的:
      • (function() {}).prototype == (function() {}).prototype // -> false
    • 所以溜腐,語法上每個function都可以被當作構(gòu)造函數(shù)來使用挺益,例如:
      • new (function(x) {this.v = x*2})(3) // -> {v: 6}
        • 上一句定義了一個匿名函數(shù)乘寒,用它作為構(gòu)造函數(shù),參數(shù)是3烂翰,創(chuàng)建了一個object,里面有一個屬性(key="v", value=6)甘耿;
      • new (function(){}) // -> {} -- 創(chuàng)建了一個空object
      • new (function(x) {x = 2})(3) // -> {} -- 空object,因為沒有通過this往object里添加屬性捏境;
  5. 除了Object這個object等少數(shù)幾個objects外,JS里每個object都有它的原型典蝌,你可以用Object.getPrototypeOf()這種標準方式來獲取头谜,或者使用__proto__這個非標準屬性來獲取(IE的JS引擎中的objects就沒有__proto__這個屬性):
    • Object.getPrototypeOf({}) === Object.prototype // -> true
    • ({}).__proto__ === Object.prototype // -> true
    • Object.getPrototypeOf(function (){}) === Function.prototype // -> true
  6. 當一個function跟著new操作符時柱告,它就被用作構(gòu)造函數(shù)來使用了笑陈,例如:
    • var clock3 = new PrettyClock("Chongqing", "RED")
    • 上面一句的new操作符實際上做了4件事情:
      • A. 創(chuàng)建一個空的臨時object,類似于 var tmp = {};涵妥;
      • B. 記錄臨時object與PrettyClock的關(guān)系(原型關(guān)系),類似于 tmp.__proto__ = PrettyClock.prototype窒所;
        • 注:這一點是瀏覽器、JS引擎的內(nèi)部機制吵取,Chrome和Node.js中有__proto__,但IE中沒有皮官;
      • C. 以"Chongqing", "RED"為參數(shù)实辑,調(diào)用PrettyClock這個構(gòu)造函數(shù)(Constructor),在PrettyClock的body內(nèi)部時this指向那個新創(chuàng)建的臨時空對象(那個tmp)剪撬;
        • 這里值得強調(diào)的是:構(gòu)造函數(shù)中的this.timezone = timezone;一類賦值語句,其實是在那個臨時object中添加或者修改實例屬性 -- 這跟Java的機制不一樣,Java是在class內(nèi)部定義了實例屬性(必要時可以同時賦初始值)问慎,而在(Java的)構(gòu)造函數(shù)中挤茄,最多是修改實例屬性值,無法添加屬性或者修改屬性的名字或者類型穷劈。
      • D. PrettyClock函數(shù)結(jié)束執(zhí)行后猫妙,返回臨時object人乓,上述語句中就賦給了clock3乌妒。
  7. 當訪問一個object的屬性(普通屬性或者方法)時追葡,步驟如下:
    • 如果本object中有這個屬性,就返回它的值宜肉,訪問結(jié)束;
    • 如果本object中沒有這個屬性翎碑,就往該object的__proto__屬性指向的object(prototype object)里找,如果找到日杈,就返回它的值遣铝,訪問結(jié)束莉擒;
    • 如果還沒找到,就不斷重復上一步驟的動作啰劲,直到?jīng)]有上級prototype梁沧,此時返回undefined這個值蝇裤;
      • 實際運行中,是一直找到Object.prototype這個object的栓辜,因為它的__proto__等于null,所以不會再繼續(xù)找了藕甩。相關(guān)信息如下:
        • typeof Object.prototype // -> "object"
        • Object.prototype.__proto__ === null // -> true
  8. 上一點提到的尋找object的某個屬性的過程周荐,就是原型鏈的工作機制僵娃,它的運行效果跟Java中“在類繼承樹上一直往上找,直到Object類中都找不到為止”差不多默怨。

四、結(jié)合代碼和對象圖解讀理解原型鏈

要理解JS的OO機制匙睹,需要先理解原型鏈(prototype chain);要理解原型鏈霎槐,則需要理解構(gòu)造函數(shù)(constructor)、原型對象(prototype object)丘跌、普通對象(object)之間的關(guān)系,說明如下碍岔。

為了避免你來回滾動網(wǎng)頁朵夏,這里把上面的對象圖再貼一次:

JavaScript原型鏈
  • 圖中每個“大框”(2到4個小方框的集合)都是一個object榆纽,大框中的每個小框(除了最上面的一個)都是一個屬性(Property),圖中列舉的這些屬性饥侵,它們的值就是箭頭指向的那個object。其中躏升,
    • 左邊一列(淺藍色)的objects都是構(gòu)造函數(shù)(Constructor),它們都是functions(概念上膨疏,可以認為Function是Object的子類)钻弄,從圖中它們的__proto__屬性都指向Function.prototype(紅色的線)就可以看出來;
    • 中間一列(淺綠色)的objects充當原型(Prototype)的角色窘俺,它們本身是objects,這從它們的__proto__屬性指向Object.prototype(青色的線)可以看得出來;
      • 充當prototype的objects都需要有一個constructor屬性(留意綠色的線)育八,指向其對應(yīng)的構(gòu)造函數(shù),這個屬性在你手工構(gòu)造prototype時髓棋,則需要自行顯式添加;
    • 右邊一列(淺橙色)的objects就是普通對象了深纲,它們本身沒有constructor或者prototype屬性,但有JS引擎內(nèi)部為它們添加的__proto__屬性來指向?qū)?yīng)的prototype對象湃鹊;
      • var clock3 = new PrettyClock("Chongqing", "RED")時, new操作符先創(chuàng)建一個臨時對象怀愧,然后把PrettyClock的prototype屬性的值(就是圖中淺綠色的PrettyClock.prototype這個object的引用)添加到這個臨時對象中(作為一個內(nèi)部屬性,名為__proto__)芯义,最后將臨時對象賦給變量clock3。
  • JS的原型鏈就是由圖中青色的線+藍色的線所構(gòu)成的扛拨,其運作機制需要用幾個例子來說明:
    • clock3.greeting(); // -> "A clock - Chongqing + RED"
      • clock3對象本身沒有名為"greeting"的屬性举塔,所以到其__proto__指向的object即PrettyClock.prototype里找,結(jié)果有央渣,所以就調(diào)用;
        • clock3.hasOwnProperty("greeting") // -> false
        • clock3.__proto__.hasOwnProperty("greeting") // -> true
      • 調(diào)用時這個greeting函數(shù)時芽丹,關(guān)鍵字this指向的是clock3這個對象,這個對象有timezonecolor兩個屬性值咕村,所以打印結(jié)果"A clock - Chongqing + RED";
        • clock3的這兩個屬性值培廓,是在用new調(diào)用PrettyClock這個構(gòu)造函數(shù)(圖中淺藍色的PrettyClock)時被添加進去的,其中timezone這個屬性是在(PrettyClock直接地)調(diào)用Clock這個構(gòu)造函數(shù)添加的 ---- 這一點很重要肩钠,請好好理解一下;
    • clock3.toString(); // -> "[object Object]"
      • clock3本身沒有"toString"這個屬性(例子中沒有添加這個屬性)价匠,估往PrettyClock.prototype找(藍線),也沒踩窖,再往Clock.prototype找(青色虛線),還是沒有洋腮,再往Object.prototype找(青色線),就找到了啥供。
      • 這里需要指出的是,你Chrome的console中能打印出一個object的一個屬性值伙狐,并不代表這個這個屬性值在這個object中,這需要用hasOwnProperty()來檢查贷屎,例子如下:
        • clock3.toString != null // -> true - 能讀到這個屬性
        • clock3.hasOwnProperty("toString") // -> false - 不直接擁有
        • clock3.__proto__.__proto__.__proto__.hasOwnProperty('toString') // -> true
          • 終于找到了
        • clock3.__proto__.__proto__.__proto__ === Object.prototype // -> true
          • 這一點,結(jié)合圖中的線來理解一下吧咒吐;
        • clock3.__proto__.__proto__ === Clock.prototype // -> true
          • 如果上面一點理解了,這一點就不在話下了渤滞。
    • Object.prototype.__proto__ === null // -> true
      • Object.prototype這個object是原型鏈的最頂端了(就如Java中Object這個class是最頂端的class一樣),所以它沒有原型了,所以它的__proto__這個屬性的值為null(注意:不是undefined陶舞,至于null與undefined的區(qū)別,請自行搜索一下)肿孵;
      • 這一點的嚴謹寫法是:
        • Object.getPrototypeOf(Object.prototype) === null // -> true
        • 再重復一遍:Chrome、Node.js中每個object都有__proto__這個屬性停做,但IE中就沒有,因為它不是JavaScript的規(guī)范的中定義的蛉腌,Object.getPrototypeOf()才是標準用法只厘。

寫到這里就差不多了舅巷。??

五、總結(jié)

JS的OO機制钠右、原型鏈不容易理解,原因有這幾點:

  1. JS的OO機制本來就復雜(沒有Java的那么簡潔)飒房;
  2. “對象”、“屬性”狠毯、“原型”這些詞的含義比較模糊,帶來了理解上的困難垃你;
  3. Object、Function皆刺、String等關(guān)鍵字其實是函數(shù),它們充當著“某個class的門面”的角色羡蛾,但它們并不直接是原型鏈的一部分,因為那些實例方法(instance method)不是放在它們里面痴怨,而是放在它們對應(yīng)的原型對象里面。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浪藻,一起剝皮案震驚了整個濱河市乾翔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌反浓,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辆雾,死亡現(xiàn)場離奇詭異,居然都是意外死亡度迂,警方通過查閱死者的電腦和手機藤乙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門湾盒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人罚勾,你說我怎么就攤上這事〖庋辏” “怎么了划煮?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長弛秋。 經(jīng)常有香客問我,道長蟹略,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任揽浙,我火速辦了婚禮,結(jié)果婚禮上馅巷,老公的妹妹穿的比我還像新娘。我一直安慰自己钓猬,他們只是感情好,可當我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布敞曹。 她就那樣靜靜地躺著跌榔,像睡著了一般捶障。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上项炼,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天示绊,我揣著相機與錄音暂论,去河邊找鬼。 笑死取胎,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的闻蛀。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼觉痛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了薪棒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤棵介,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鞍时,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡逆巍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年莽使,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芳肌。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖翎迁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情汪榔,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布痴腌,位于F島的核電站雌团,受9級特大地震影響锦援,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜剥悟,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望替久。 院中可真熱鬧,春花似錦蚯根、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽距帅。三九已至,卻和暖如春碌秸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背讥电。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工轧抗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人横媚。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像灯蝴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子穷躁,可洞房花燭夜當晚...
    茶點故事閱讀 44,960評論 2 355

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