JavaScript 高級程序設(shè)計(jì)(二)

P4 面向?qū)ο蟮某绦蛟O(shè)計(jì)

4.1 理解對象

// 創(chuàng)建實(shí)例
var person = new Object();
person.name = 'hqy';
person.age = 21;
person.sayName = function() {
    alert(this.name);
}

// 對象字面量
var person = {
    name: 'hqy',
    age: 21,
    sayName; function() {
        alert(this.name);
    }
}

數(shù)據(jù)屬性

  • Configurable:能否通過 delete 刪除屬性,能否修改屬性的特性,能否把屬性修改為訪問器屬性,默認(rèn) true
  • Enumerable:能否通過for-in循環(huán)返回屬性,默認(rèn) true
  • Writable:能否修改屬性的值,默認(rèn) true
  • Value:這個屬性的數(shù)據(jù)值,默認(rèn) undefined

使用ES5的 Object.defineProperty() 修改屬性默認(rèn)的特性

// 參數(shù):屬性所在對象,屬性名,一個描述符對象
var person = {};
Object.defineProperty(person, 'name', {
    writable: false, // 設(shè)置成只讀
    value: 'hqy'
})
alert(person.name) // hqy
person.name = 'sss'; // 嚴(yán)格模式下報錯
alert(person.name) // hqy

// 將 configurable 設(shè)置成 false,delete 無效,嚴(yán)格模式報錯
// 一旦把屬性定義為不可配置的,就不能再把它變回可配置了,修改除 writable 之外的特性都會導(dǎo)致錯誤

訪問器屬性

  • Configurable:能否通過 delete 刪除屬性,能否修改屬性的特性,能否把屬性修改為訪問器屬性,默認(rèn) true
  • Enumerable:能否通過for-in循環(huán)返回屬性,默認(rèn) true
  • Get:讀取屬性時調(diào)用的函數(shù),默認(rèn) undefined
  • Set:寫入屬性時調(diào)用的函數(shù),默認(rèn) undefined

訪問器屬性不能直接定義,必須使用 Object.defineProperty() 來定義

var book = {
    _year: 2004, // 下劃線常用來表示只能通過對象方法訪問的屬性
    edition: 1
};
Object.defineProperty(book, 'year', {
    get: function() {
        return this._year;
    },
    set: function(val) {
        if(val > 2004) {
            this._year = val;
            this.edition += val - 2004;
        }
    }
})
book.year = 2005;
alert(book.edition) // 2

讀取屬性的特性
使用 Object.getOwnPropertyDescriptor(對象,屬性名) 獲得給定屬性的描述符

4.2 創(chuàng)建對象-工廠模式

function createPerson(name, age) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayName = function() {
        alert(this.name);
    }
    return o;
} 

缺點(diǎn):沒有解決對象識別的問題,即怎么知道一個對象的類型

4.2 創(chuàng)建對象-構(gòu)造函數(shù)模式

// 構(gòu)造函數(shù)始終都應(yīng)該以一個大寫字母開頭
function  Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function() {
        alert(this.name);
    }
}
var person1 = new Person('hqy', 21);
var person2 = new Person('sss', 22);

與工廠模式相比:1. 沒有顯式創(chuàng)建對象, 2. 直接將屬性和方法賦給this對象,3. 沒有return
要創(chuàng)建Person的新實(shí)例,必須使用new

  • 創(chuàng)建一個新對象
  • 將構(gòu)造函數(shù)的作用域賦給新對象(this指向這個新對象)
  • 執(zhí)行構(gòu)造函數(shù)中的代碼(為這個新對象添加屬性)
  • 返回新對象

最后,person1person2 分別報錯著 Person 的一個不同的實(shí)例,每個對象都有一個 constructor 屬性,指向 Person。

alert(person1.constructor == Person) // true
alert(person2.constructor == Person) // true

// 檢測對象類型
alert(person1 instanceof Person) // true
alert(person1 instanceof Object) // true

將構(gòu)造函數(shù)當(dāng)作函數(shù)

// 當(dāng)構(gòu)造函數(shù)
var person = new Person('hqy', 21);
person.sayName() // hqy

// 當(dāng)普通函數(shù)
Person('hqy', 21); // 添加到 window
window.sayName(); // 'hqy'

// 在另一個對象的作用域中調(diào)用
var o = new Object();
Person.call(o, 'sss', 25);
o.sayName() // sss

缺點(diǎn)
每個方法都要在每個實(shí)例上重新創(chuàng)建一遍,即不同實(shí)例上的同名函數(shù)是不相等的

alert(person1.sayName == person.sayName) // false

解決辦法

function  Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
function sayName() {
    alert(this.name);
}

但這樣如果對象需要定義很多方法,那么就要定義這么多的全局函數(shù)。。。

4.2 創(chuàng)建對象-原型模式 ★

理解原型對象

  • 每個函數(shù)都有 prototype,指向函數(shù)的原型對象
  • 每個原型對象都有 constructor (構(gòu)造函數(shù)),指向 prototype 屬性所在函數(shù)的指針
  • Person.prototype.constructor 指向 Person
  • 每個對象可以用 __proto__ 訪問 Prototype
  • 這個連接存在與實(shí)例與構(gòu)造函數(shù)的原型對象之間,而不是存在于實(shí)例與構(gòu)造函數(shù)之間
image.png

確定對象間是否存在這種關(guān)系

// 通過 isPrototypeOf()
alert(Person.prototype.isPrototypeOf(person1)) // true

// Object.getPrototypeOf()
alert(Object.getPrototypeOf(person1) == Person.prototype) // true
alert(Object.getPrototypeOf(person1).name) // name 設(shè)置在原型上

每當(dāng)代碼讀取某個對象的某個屬性時,都會執(zhí)行一次搜索,目標(biāo)是具有給定名字的屬性。

  • 搜索首先從實(shí)例開始。如果實(shí)例上找到了具有同名的屬性。則返回該屬性的值
  • 上面沒有找到,繼續(xù)搜索指針指向的原型對象,找到返回,沒找到返回 undefined
image.png

不能通過對象實(shí)例重寫原型中的值,也就是說實(shí)例上的屬性怎么操作都不會影響到原型

function Person() {}
Person.prototype.name = 'hqy';
var person1 = new Person();
var person2 = new Person();
person1.name = 'sss';
alert(person1.name) // 'sss'
alert(person2.name) // 'hqy'

// 只有在實(shí)例上刪除該屬性才能重新訪問到原型中的屬性
delete person1.name;
alert(person1.name) // 'hqy'

hasOwnProperty() 方法檢測一個屬性是存在于實(shí)例上還是原型上,只在給定屬性存在于實(shí)例上才會返回 true。
in 單獨(dú)使用時會在通過對象能夠訪問給頂屬性時返回 true,無論在實(shí)例上還是原型中。

// 判斷屬性到底是存在于對象中還是實(shí)例上
function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object); 
}

枚舉屬性 Object.keys(),Object.getOwnPropertyName()

function Person() {}
Person.prototype.name = 'hqy';
Person.prototype.age = 21;
Person.prototype.job = 'software engineer';

var key = Object.keys(Person.prototype);
alert(key) // name,age,job

var person = new Person();
person.name = 'bob';
var key = Object.keys(person);
alert(key) // name

var key = Object.getOwnPropertyName(Person.prototype);
alert(key) // constructor,name,age,job

更簡單的原型語法

function Person() {}
Person.prototype = {
    constructor: Person
    // 以字面量創(chuàng)建,constructor 不再指向 Person
    name: 'hqy',
    age: 21,
    job: 'software engineer'
}

原型動態(tài)性

  • 重寫整個原型對象,會切斷構(gòu)造函數(shù)與最初原型之間的聯(lián)系
  • 實(shí)例中的指針僅指向原型,不指向構(gòu)造函數(shù)
function Person() {}

var friend = new Person();

// 重寫整個原型對象,會切斷構(gòu)造函數(shù)與最初原型之間的聯(lián)系
Person.prototype = {
    constructor: Person
    // 以字面量創(chuàng)建,constructor 不再指向 Person
    name: 'hqy',
    age: 21,
    job: 'software engineer',
    sayName: function() {
        alert(this.name);
    }
}
friend.sayName(); // error
image.png

原生對象的原型

alert(typeof Array.prototype.sort) // function

String.prototype.getLength = function() {
    return this.length;
}

缺點(diǎn)

function Person() {}

Person.prototype = {
    constructor: Person
    name: [1, 2],   // 該屬性會被實(shí)例共享
    sayName: function() {
        alert(this.name);
    }
}

var person1 = new Person();
var person2 = new Person();

person1.push(3);
alert(person1.sayName); // 1,2,3
alert(person2.sayName); // 1,2,3
alert(person1.name === person2.name) // true

4.2 創(chuàng)建對象-構(gòu)造函數(shù)+原型模式

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.friend = [1, 2];
}

Person.prototype = {
    constructor: Person
    sayName: function() {
        alert(this.name);
    }
}

var person1 = new Person();
var person2 = new Person();

person1.push(3);
alert(person1.sayName); // 1,2,3
alert(person2.sayName); // 1,2
alert(person1.name === person2.name) // false

4.2 創(chuàng)建對象-動態(tài)原型模式

通過檢查某個應(yīng)該存在的方法是否有效,來決定是否需要初始化原型

function Person(name) {
    // 屬性
    this.name = name;
    //方法
    // 初次調(diào)用構(gòu)造函數(shù)時才會執(zhí)行
    if(typeof this.sayName !== 'function') {
        Person.prototype.sayName = function() {
            return this.name;
        }
    }
}

4.2 創(chuàng)建對象-寄生構(gòu)造函數(shù)模式

寄生構(gòu)造函數(shù)返回的對象與構(gòu)造函數(shù)或者構(gòu)造函數(shù)的原型屬性之間沒有關(guān)系,不能依賴 instanceof 確定對象類型。

// 創(chuàng)建一個具有額外方法的特殊數(shù)組
function SpecialArray() {
    var list = new Array();
    list.push.apply(list, arguments);
    list.toPipedString = function() {
        return this.join('|');
    }
    return list;
}
var color = new SpecialArray(1, 2, 3);
alert(color.toPipedString()) // 1|2|3

4.2 創(chuàng)建對象-穩(wěn)妥構(gòu)造函數(shù)模式

穩(wěn)妥對象最適合在一些安全的環(huán)境中(這些環(huán)境禁止使用thisnew),或者在防止數(shù)據(jù)被其他應(yīng)用程序改動時使用。

  • 新創(chuàng)建對象的實(shí)例方法不引用 this
  • 不使用 new 操作符調(diào)用構(gòu)造函數(shù)
function Person(name) {
    var o = new Object();
    // 可以在這里定義私有變量和函數(shù)
    ...
    // 函數(shù)方法
    o.sayName = function() {
        alert(name);
    }
    return o;
}
var person = Person('hqy');
person.sayName() // hqy

除了調(diào)用 sayName(),否則沒其他方法訪問其數(shù)據(jù)成員

4.3 繼承-原型鏈

構(gòu)造函數(shù)、原型和實(shí)例的關(guān)系:
每個構(gòu)造函數(shù)都有一個原型對象,原型對象都包含一個指向構(gòu)造函數(shù)的指針,而實(shí)例都包含一個指向原型對象的內(nèi)部指針。

假如我們讓原型對象等于另一個類型的實(shí)例:
此時的原型對象將包含一個指向另一個原型的指針,相應(yīng)的,另一個原型也包含著一個指向另一個構(gòu)造函數(shù)的指針。
假如另一個原型有事另一個類型的實(shí)例,那么上面的關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實(shí)例和原型的鏈條,這就是所謂的原型鏈。

function SuperType() {
    this.property = true;
}
SuperType.prototype.getSuperValue = function() {
    return this.property;
}
function SubType() {
    this.subproperty = false;
}
// 繼承 SubType
SubType.prototype = new SuperType();
SubType.protptype.getSubValue = function() {
    return this.subproperty;
}

var instance = new SubType();
alert(instance.getSuperValue()) // true

instance 指向SubType 的原型,SubType 的原型又指向 SuperType 的原型。
getSuperValue() 方法仍然還在SuperType.protptype中,但property則位于SubType.prototype中。這是因?yàn)?code>property是一個實(shí)例屬性,而getSuperValue()是個原型方法。

image.png

確定原型和實(shí)例的關(guān)系

// instacnceof
alert(instance instanceof Object) // true
alert(instance instanceof SuperType) // true
alert(instance instanceof SubType) // true

// isPropertyOf()
alert(Object.prototype.isPropertyOf(instance)) // true
alert(SuperType.prototype.isPropertyOf(instance)) // true
alert(SubType.prototype.isPropertyOf(instance)) // true

謹(jǐn)慎地定義方法

  • 給原型添加方法一定能夠要放在替換原型的語句之后
  • 在通過原型鏈實(shí)現(xiàn)繼承時,不能使用對象字面量創(chuàng)建原型方法,這樣做會重寫原型鏈,切斷構(gòu)造函數(shù)和原型的聯(lián)系

原型鏈的缺點(diǎn)
包含引用類型值的原型屬性會被所有實(shí)例共享。(4.2 創(chuàng)建對象-構(gòu)造函數(shù)+原型模式 缺點(diǎn)已介紹)

4.3 繼承-借用構(gòu)造函數(shù)

function SuperType(name) {
    this.name = name;
}
function SubType(name) {
    // 繼承了 SuperType,同時傳遞參數(shù)
    SuperType.call(this, name);

    // 實(shí)例屬性
    this.age = 21;
}
var instance = new SubType('hqy')
alert(instance.name); // hqy
alert(instance.age); // 21

缺點(diǎn)
僅僅是借用構(gòu)造函數(shù),方法都在構(gòu)造函數(shù)中定義,因此函數(shù)復(fù)用就無從談起了。

4.3 繼承-組合繼承(借用構(gòu)造+原型鏈)

使用原型鏈實(shí)現(xiàn)對原型屬性和方法的繼承,而通過借用構(gòu)造函數(shù)實(shí)現(xiàn)對實(shí)例屬性的繼承

function SuperType(name) {
    this.name = name;
    this.colors = [1, 2, 3];
}
SuperType.prototype.sayName = function() {
    return this.name;
}
function SubType(name, age) {
    // 繼承屬性
    SuperType.call(this, name);
    this.age = age;
}
// 繼承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType; // ★
SubType.prototype.sayAge = function() {
    return this.age;
}

var instance1 = new SubType('hqy', 21);
instance1.colors.push(4);
alert(instance1.colors) // 1,2,3,4
alert(instance.sayName()) // hqy
alert(instance.sayAge()) // 21

var instance2 = new SubType('sss', 25);
instance1.colors.push(4);
alert(instance1.colors) // 1,2,3
alert(instance.sayName()) // sss
alert(instance.sayAge()) // 2251

4.3 繼承-原型式繼承

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

object 函數(shù)內(nèi)部,先創(chuàng)建了一個臨時性的構(gòu)造函數(shù),然后將傳入額對象作為這個構(gòu)造函數(shù)的原型,最后返回這個臨時類型的一個新實(shí)例。

var person = {
    name: 'hqy',
    friends: [1, 2, 3, 4]
}
var a = object(person);
// 等價于 var a = Object.create(person);
a.friends.push(5);

var b = object(person);
a.friends.push(6);

alert(person.friends) // 1, 2, 3, 4, 5, 6
  • 返回的這個新對象將 person 作為原型,所以它的原型中就包含一個基本類型值屬性和一個引用類型值屬性。
  • 這意味著 person.friends 不僅屬于 person 所有,而且也會被 ab 共享。
  • 包含引用類型值的屬性始終都會共享相應(yīng)的值

4.3 繼承-寄生式繼承

該函數(shù)在內(nèi)部以某種方式來增強(qiáng)對象

fucntion createAnother(origin) {
    var clone = object(origin); // 創(chuàng)建一個新對象
    clone.sayHi = function() {  // 以某種方式來增強(qiáng)對象
        alert('hi');
    }
    return clone; // 返回這個對象
}

var person = {
    name: 'hqy',
    friends: [1, 2, 3, 4]
}
var a = createAnother(person);
a.sayHi(); // hi

4.3 繼承-寄生組合繼承

組合繼承是 JavaScript 最常用的繼承模式,但它最大的問題在無論什么情況下,都會調(diào)用兩次超類型構(gòu)造函數(shù)

  • 創(chuàng)建子類型原型時
  • 子類型構(gòu)造函數(shù)內(nèi)部

組合繼承的例子

function SuperType(name) {
    this.name = name;
    this.colors = [1, 2, 3];
}
SuperType.prototype.sayName = function() {
    return this.name;
}
function SubType(name, age) {
    // 繼承屬性
    SuperType.call(this, name); // 第二次調(diào)用 SuperType()
    this.age = age;
}
// 繼承方法
SubType.prototype = new SuperType(); // 第一次調(diào)用 SuperType()
SubType.prototype.constructor = SubType; // ★
SubType.prototype.sayAge = function() {
    return this.age;
}

寄生組合式繼承,即通過借用構(gòu)造函數(shù)來繼承屬性,通過原型鏈的混成形式來繼承方法。
思路:不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù),我們需要的無非就是超類型原型的一個副本而已

// 參數(shù):子類型構(gòu)造函數(shù),超類型構(gòu)造函數(shù)
function inheritPrototype(subType, superType) {
    var prototype = object(superType.prototype); // 創(chuàng)建超類型原型的一個副本
    prototype.constructor = subType; // 增強(qiáng)對象,為副本添加constructor屬性
    subType.protptype = prototype; // 將副本復(fù)制給子類型的原型
}

function SuperType(name) {
    this.name = name;
    this.colors = [1, 2, 3];
}
SuperType.prototype.sayName = function() {
    return this.name;
}
function SubType(name, age) {
    // 繼承屬性
    SuperType.call(this, name);
    this.age = age;
}
// 繼承方法
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
    return this.age;
}
image.png

P5 函數(shù)表達(dá)式

  • 函數(shù)聲明提升,意思是在執(zhí)行代碼之前會項(xiàng)讀取函數(shù)聲明,但函數(shù)表達(dá)式只能當(dāng)代碼執(zhí)行到它那一行后才會被解析。
  • 創(chuàng)建一個函數(shù)并把它賦值給變量 functionName,這種情況下創(chuàng)建的函數(shù)叫做匿名函數(shù)(拉姆達(dá)函數(shù))

5.1 遞歸

arguments.callee 是一個指向正在執(zhí)行的函數(shù)的指針

function factorial(num) {
    if(num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num - 1);
        // 等價于 num * factorial(num -1),主要是為了防止函數(shù)名改變
    }
}

5.2 閉包

閉包:是指有權(quán)訪問另一個函數(shù)作用域的變量的函數(shù)。
創(chuàng)建閉包最常見的方式,就是在一個函數(shù)內(nèi)部創(chuàng)建另一個函數(shù)

function createComparisonFunction(key) {
    return function(obj1, obj2) {
        var val1 = obj1[key];
        var val2 = obj2[key];
        if (val1 < val2) {
            return -1;
        } else if(val1 > val2) {
            return 1;
        } else {
            return 0;
        }
    }
}

當(dāng)一個函數(shù)被調(diào)用時,會創(chuàng)建一個執(zhí)行環(huán)境及相應(yīng)的作用域鏈。然后,使用arguments和其他命名參數(shù)的值來初始化函數(shù)的活動對象。但在作用域中,外部函數(shù)的活動對象始終處于第二位,外部函數(shù)的外部函數(shù)的活動對象處于第三位。。。直至作為作用域鏈重點(diǎn)的全局執(zhí)行環(huán)境。

作用域鏈本質(zhì)上是一個指向變量對象的指針列表,它值引用當(dāng)不實(shí)際包含變量對象。

在另一個函數(shù)內(nèi)部定義的函數(shù)會將包含函數(shù)(外部函數(shù))的活動對象添加到它的作用域鏈中。因此,在createComparisonFunction()函數(shù)內(nèi)部定義的匿名函數(shù)的作用域中,實(shí)際上將會包含外部函數(shù)createComparisonFunction()的活動對象。

image.png

createComparisonFunction() 函數(shù)執(zhí)行完成后,其執(zhí)行環(huán)境的作用域鏈會被銷毀,但活動對象仍然留在內(nèi)存中,知道匿名函數(shù)被銷毀后,createComparisonFunction() 的活動對象才會被銷毀。

var compareNames = createComparisonFunction('name') ;
var result = compareNames({ name: 'hqy' }, { name: 'sss' });

// 解除對匿名函數(shù)的引用,一遍釋放內(nèi)存
compareNames = null;

閉包和變量

function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = function() {
            return i;
        }
    }
    return result;
}
// 每個函數(shù)都返回 10
// 因?yàn)槊總€函數(shù)的作用域中都保存著createFunction的活動對象,所以它們引用的是同一個 i

// 返回一個立即執(zhí)行的匿名函數(shù)解決問題
function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = function(num) {
            return function() {
                return num;
            }
        }(i);
    }
    return result;
}

關(guān)于 this 對象
這里只是簡單地介紹了下

var name = 'the window';

var object = {
    name: 'the object',
    getName: function() {
        return function() {
            return this.name;
        }
    }
}

// 內(nèi)部函數(shù)在搜索 this, arguments 時,只會搜索到其活動對象為止
alert(object.getName()()) // 'the window' (非嚴(yán)格模式下)

要想獲取 object 中的 name,這樣做

var name = 'the window';

var object = {
    name: 'the object',
    getName: function() {
        var that = this;
        return function() {
            return that.name;
        }
    }
}
alert(object.getName()()) // 'the object'

幾種特殊情況

var name = 'the window';

var object = {
    name: 'the object',
    getName: function() {
        return this.name;
    }
}

object.getName() // 'the object'
(object.getName)() // 'the object',和上面的代碼定義是一樣的
(object.getName = object.getName)() // 'the window' (非嚴(yán)格模式下),因?yàn)橄葓?zhí)行的時賦值語句

內(nèi)存泄漏

function handle() {
    var element = document.getElementById('someElement');
    var id = element.id; // 消除循環(huán)引用
    element.onclick = function() {
        alert(id);
    }
    element = null; // 解除對DOM對象的引用,順利減少去引用數(shù)
}

5.3 模塊模式-單例

指只有一個實(shí)例的對象

var singleton = {
    name: value,
    method: function() {
        // 這里是方法的代碼
    }
}

模塊模式通過為單例添加私有變量和特權(quán)方法能夠使其得到增強(qiáng)

var singleton = function() {
    // 私有變量和私有函數(shù)
    var privateVal = 10;
    function privateFunc() {
        return false;
    }
    //特權(quán)/公有方法和屬性
    return {
        publicProperty: true,
        publicMethod: function() {
            privateVal++;
            console.log(privateVal);
            return privateFunc();
        }
    }
}

var a = new singleton();
a.publicMethod(); // 11
a.publicMethod(); // 12
var b = new singleton();
b.publicMethod(); // 11

5.3 模塊模式-增強(qiáng)的模塊模式

var singleton = function() {
    // 私有變量和私有函數(shù)
    var privateVal = 10;
    function privateFunc() {
        return false;
    }

    // 創(chuàng)建對象
    var obj = new CustomType();

    //添加特權(quán)/公有方法和屬性
    obj.publicProperty = true;
    
    obj.publicMethod = function() {
        privateVal++;
        console.log(privateVal);
        return privateFunc();
    }

    // 返回這個對象
    return obj;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容