JS高程筆記 —— 面向?qū)ο?/h1>

面向?qū)ο蟮恼Z(yǔ)言有一個(gè)標(biāo)志,那就是它們都有的概念,而通過(guò)類可以創(chuàng)建任意多個(gè)具有相同屬性和方法的對(duì)象。ECMAScript中沒(méi)有類的概念,因此它的對(duì)象也和基于類的語(yǔ)言中的對(duì)象有所不同。

ECMA-262把對(duì)象定義為:無(wú)序?qū)傩缘募希鋵傩钥梢园局怠?duì)象或者函數(shù)。我們可以把ECMAScript的對(duì)象想象成散列表:無(wú)非就是一組key/value,其中value可以是數(shù)據(jù)或者函數(shù)。

理解對(duì)象

var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";

person.sayName = function() {
    console.log(this.name);
}

早期的JS開(kāi)發(fā)者經(jīng)常使用這個(gè)模式創(chuàng)建新對(duì)象。幾年后,對(duì)象字面量成為創(chuàng)建這種對(duì)象的首選模式。

用對(duì)象字面量創(chuàng)建上例對(duì)象如下:

var person = {
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function () {
        console.log(this.name);
    }
};
屬性類型

ECMAScript中有兩種屬性:數(shù)據(jù)屬性和訪問(wèn)器屬性。

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

包含一個(gè)數(shù)據(jù)值的位置。在這個(gè)位置可以讀取和寫入值。數(shù)據(jù)屬性有4個(gè)描述行為的特性。

  • [[Configurable]]:表示能否通過(guò)delete刪除屬性從而重新定義屬性,能否修改屬性特性,能否把屬性修改為訪問(wèn)器屬性。
    特性默認(rèn)值:true

  • [[Enumerable]]:表示能否通過(guò)for-in循環(huán)返回屬性。
    特性默認(rèn)值:true

  • [[Writable]]:表示能否修改屬性值(可寫)。
    特性默認(rèn)值:true

  • [[Value]]:表示這個(gè)屬性的數(shù)據(jù)值。讀取屬性時(shí),從這個(gè)位置讀;寫入屬性值,把新值保存在這。
    特性默認(rèn)值:true

直接使用對(duì)面字面量定義的屬性,他們的數(shù)據(jù)屬性全部為true

var person = {
    name: "Nicholas"
}

這里創(chuàng)建的名為name的屬性,指定的值為"Nicholas"。也就是說(shuō),[[Value]]特性將被設(shè)置為"Nicholas",而對(duì)這個(gè)值任何修改都將反應(yīng)在這個(gè)位置。

var person = {
    name: "Nicholas"
}

// Writable
person.name = "John";       
console.log(person.name);       // John
// Enumerable
for (var i in person) {
    console.log(person[i]);     // John
}
// Configurable
delete person.name;
console.log(person.name);       // undefined

當(dāng)我們需要修改數(shù)據(jù)屬性默認(rèn)值時(shí),必須使用ECMAScript的Object.defineProperty()方法。

Object.defineProperty() 方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性, 并返回這個(gè)對(duì)象。

Object.defineProperty(obj, prop, descriptor)

  • obj:要在其上定義屬性的對(duì)象。
  • prop:要定義或修改的屬性的名稱。
  • descriptor:將被定義或修改的屬性描述符

這個(gè)方法接受三個(gè)參數(shù):屬性所在對(duì)象、屬性名字、一個(gè)描述符對(duì)象。
其中,描述符(descriptor)對(duì)象屬性必須是:configurable,enumerable,writable和value。設(shè)置其中的一或多個(gè)值。

var person = {};
Object.defineProperty(person,"name",{
    writable: false,
    value: "Nicholas"
});

console.log(person.name);       // "Nicholas"
person.name = "John";
console.log(person.name);       // "Nicholas"

在這里,name屬性被設(shè)置為只讀(writable:false),如果嘗試為它指定新值。
嚴(yán)格模式:拋出錯(cuò)誤
非嚴(yán)格模式:賦值操作將被忽略。

類似規(guī)則也適用于不可配制的屬性。例如:

var person = {};
Object.defineProperty(person,"name",{
    configurable: false,
    value: "Nicholas"
});

console.log(person.name);       // "Nicholas"
person.name = "John";
console.log(person.name);       // "Nicholas"

configurable設(shè)置為false,表示不能從對(duì)象中刪除屬性。
還記著之前的

  • [[Configurable]]:表示能否通過(guò)delete刪除屬性從而重新定義屬性,能否修改屬性特性,能否把屬性修改為訪問(wèn)器屬性。
    特性默認(rèn)值:true

如果對(duì)這個(gè)屬性值調(diào)用delete,則在非嚴(yán)格模式下什么會(huì)不會(huì)發(fā)生,嚴(yán)格模式下會(huì)導(dǎo)致錯(cuò)誤。

并且,一旦把屬性定義為不可配置的,就不能再把它變回可配置了。此時(shí),再調(diào)用Object.defineProperty()方法修改除writable之外的特性,都會(huì)導(dǎo)致錯(cuò)誤。

var person = {};
Object.defineProperty(person,"name",{
    configurable: false,
    value: "Nicholas"
});

// 拋出錯(cuò)誤
Object.defineProperty(person,"name",{       // TypeError: Cannot redefine property: name
    configurable: true,
    value: "Nicholas"
});

也就是說(shuō),可以多次調(diào)用Object.defineProperty()方法修改同一個(gè)屬性,但在把configurable設(shè)置為false之后就會(huì)有限制了。

另外,在configurable:false情況下,在當(dāng)writable:false想要設(shè)置為true時(shí),也會(huì)報(bào)錯(cuò)。

var person = {};
Object.defineProperty(person,"name",{
    configurable: false,
    writable: false,
    value: "Nicholas"
});

person.name = "John";
console.log(person.name);           // "Nicholas"

Object.defineProperty(person,"name",{   // TypeError: Cannot redefine property: name
    writable: true
});
var person = {};
Object.defineProperty(person,"name",{
    configurable: false,
    writable: true,
    value: "Nicholas"
});

person.name = "John";
console.log(person.name);               // "John"

Object.defineProperty(person,"name",{
    writable: false
});
Object.defineProperty(person,"name",{   // TypeError: Cannot redefine property: name
    writable: true
});

調(diào)用Object.defineProperty()創(chuàng)建一個(gè)新屬性時(shí),如果不指定,configurable,enumerable,writable默認(rèn)值都是false。

var person = {};
Object.defineProperty(person,"name",{
    value: "Nicholas"
});

console.log(person.name);       // Nicholas
person.name = "John";
console.log(person.name);       // Nicholas    說(shuō)明writable:false

for (var i in person) {         // enumerable:false
    console.log(i);
}

多數(shù)情況下,可能沒(méi)有必要利用Object.defineProperty()方法提供了這些高級(jí)功能,不過(guò),理解這些概念對(duì)理解JS對(duì)象非常有用。

1. 訪問(wèn)器屬性

訪問(wèn)器屬性不包含數(shù)據(jù)值;它們包含一對(duì)兒getter和setter函數(shù)(不過(guò),這兩個(gè)函數(shù)都不是必需的)。在讀取訪問(wèn)器屬性時(shí),會(huì)調(diào)用getter函數(shù),這個(gè)函數(shù)負(fù)責(zé)返回有效的值;在寫入訪問(wèn)器屬性時(shí),會(huì)調(diào)用setter函數(shù)并傳入新值,這個(gè)函數(shù)負(fù)責(zé)決定如何處理數(shù)據(jù)。訪問(wèn)器屬性有如下4個(gè)特性:

  • [[Configurable]]:表示能否通過(guò)delete刪除屬性從而重新定義屬性,能否修改屬性特性,能否把屬性修改為數(shù)據(jù)屬性。
    特性默認(rèn)值:true

  • [[Enumerable]]:表示能否通過(guò)for-in循環(huán)返回屬性。
    特性默認(rèn)值:true

  • [[Get]]:在讀取屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值:undefined

  • [[Set]]:在寫入屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值:undefined

var book = {
    _year: 2004,
    edition: 1
};

Object.defineProperty(book,"year",{     // year作為訪問(wèn)器屬性,內(nèi)部方法實(shí)現(xiàn)讀取和寫入
    get: function() {
        return this._year;
    },
    set: function(newValue) {
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    }
});

console.log(book.edition);  // 1
book.year = 2005;           
console.log(book.edition);  // 2

_year前面的下劃線是一種常用記號(hào),用戶表示只能通過(guò)對(duì)象方法訪問(wèn)的屬性。
訪問(wèn)器屬性year則包含一個(gè)getter函數(shù)和一個(gè)setter函數(shù)。getter函數(shù)返回_year的值,setter函數(shù)通過(guò)計(jì)算來(lái)確定正確的版本。因此,把year屬性修改為2005會(huì)導(dǎo)致_year變成2005,而edition變?yōu)?。這是使用訪問(wèn)器屬性的常見(jiàn)方式,即設(shè)置一個(gè)屬性的值會(huì)導(dǎo)致其他屬性發(fā)生變化。

PS:有同學(xué)看到這覺(jué)得疑問(wèn),為什么給book.year會(huì)調(diào)用內(nèi)部的函數(shù)?還記得之前這句話嗎?

在讀取訪問(wèn)器屬性時(shí),會(huì)調(diào)用getter函數(shù),這個(gè)函數(shù)負(fù)責(zé)返回有效的值;
在寫入訪問(wèn)器屬性時(shí),會(huì)調(diào)用setter函數(shù)并傳入新值,這個(gè)函數(shù)負(fù)責(zé)決定如何處理數(shù)據(jù)。

所以,叫“訪問(wèn)器屬性”啦23333。
相比之前的數(shù)據(jù)屬性,從名字可以一窺全豹。
數(shù)據(jù)屬性多用于存放數(shù)據(jù),訪問(wèn)器屬性則多了兩個(gè)可以讀寫的getter函數(shù)與setter函數(shù)。

當(dāng)然,不一定非要同時(shí)指定getter函數(shù)和setter函數(shù)。

  • 只指定getter意味著屬性不能寫,嘗試寫入屬性會(huì)被忽略,嚴(yán)格模式下會(huì)報(bào)錯(cuò)。
  • 只指定setter意味著屬性不能讀,否則在非嚴(yán)格模式下會(huì)返回undefined,嚴(yán)格模式下會(huì)報(bào)錯(cuò)。

定義多個(gè)屬性

Object.defineProperties() 方法直接在一個(gè)對(duì)象上定義新的屬性或修改現(xiàn)有屬性,并返回該對(duì)象。

Object.defineProperties(obj, props)

  • obj:在其上定義或修改屬性的對(duì)象。
  • props:要定義其可枚舉屬性或修改的屬性描述符的對(duì)象。對(duì)象中存在的屬性描述符主要有兩種:數(shù)據(jù)描述符和訪問(wèn)器描述符.
var book = {
    _year: 2004,
    edition: 1
};

Object.defineProperties(book,{
    _year: {
        writable: true,
        value: 2004
    },
    edition: {
        writable: true,
        value: 1
    },
    year: {
        get: function() {
            return this._year;
        }
    },
    set: function(newValue) {
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    }
});

上例代碼在book對(duì)象上定義了兩個(gè)數(shù)據(jù)屬性(_year和edition)和一個(gè)訪問(wèn)器屬性(year)。


讀取屬性的特性

Object.getOwnPropertyDescriptor(obj, prop)

  • obj:需要查找的目標(biāo)對(duì)象
  • prop:目標(biāo)對(duì)象內(nèi)屬性名稱(String類型)
var book = {
    _year: 2004,
    edition: 1
};

Object.defineProperties(book,{
    _year: {
        value: 2004
    },
    edition: {
        value: 1
    },
    year: {
        get: function() {
            return this._year;
        },
        set: function(newValue) {
            if (newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    }
});

var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
console.log(descriptor.value);          // 2004
console.log(descriptor.configurable);   // true
console.log(typeof descriptor.get);     // undefined

var descriptor = Object.getOwnPropertyDescriptor(book,"year");
console.log(descriptor.value);          // undefined
console.log(descriptor.enumerable);     // false
console.log(typeof descriptor.get);     // function

對(duì)于數(shù)據(jù)屬性_year,value等于最初的值,configurable是false,而get等于undefined。對(duì)于訪問(wèn)器屬性year,value等于undefined,enumerable是false,而get是一個(gè)指向getter函數(shù)的指針。

在JavaScript中,可以針對(duì)任何對(duì)象——包括DOM對(duì)象和DOM對(duì)象,使用Object.getOwnProperty-Descriptor()方法。


創(chuàng)建對(duì)象

工廠模式

舊時(shí)的開(kāi)發(fā)人員在考慮到ES中無(wú)法創(chuàng)建類,開(kāi)發(fā)人員就發(fā)明了一種函數(shù),用函數(shù)來(lái)封裝以特定接口創(chuàng)建對(duì)象的細(xì)節(jié)。

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

var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

工廠模式的解決了創(chuàng)建多個(gè)相似對(duì)象的問(wèn)題,但卻沒(méi)有對(duì)象識(shí)別的問(wèn)題(即怎么樣知道一個(gè)對(duì)象的類型)。

構(gòu)造函數(shù)模式

ECMAScript中的構(gòu)造函數(shù)可用來(lái)創(chuàng)建特定類型的對(duì)象。像Object和Array這樣的原生構(gòu)造函數(shù),在運(yùn)行時(shí)會(huì)自動(dòng)出現(xiàn)在執(zhí)行環(huán)境中。此外,也可以創(chuàng)建自定義的構(gòu)造函數(shù)。從而定義自定義對(duì)象類型的屬性和方法。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () {
        console.log(this.name);
    }
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

在這個(gè)例子中,Person()函數(shù)取代了createPerson()函數(shù),Person()中的代碼除了與createPerson()中相同的部分外,還存在以下特別之處。

  • 沒(méi)有顯式創(chuàng)建對(duì)象
  • 直接將屬性和方法賦給了this對(duì)象;
  • 沒(méi)有return語(yǔ)句

此外,函數(shù)名使用大寫字母開(kāi)頭。按照慣例,構(gòu)造函數(shù)始終都應(yīng)該以一個(gè)大寫字母開(kāi)頭,而非構(gòu)造函數(shù)則應(yīng)該以一個(gè)小寫字母開(kāi)頭。這個(gè)做法借鑒了其他OO語(yǔ)言,主要是為了區(qū)別ES中的其他函數(shù);因?yàn)闃?gòu)造函數(shù)本身也是函數(shù),只不過(guò)可以用來(lái)創(chuàng)建對(duì)象而已。

創(chuàng)建Person的新實(shí)例,必須使用new操作符。以這種方式調(diào)用的構(gòu)造函數(shù)會(huì)經(jīng)歷以下4個(gè)步驟:

  1. 創(chuàng)建一個(gè)新對(duì)象(隱式)
  2. 將構(gòu)造函數(shù)的作用域賦值給新對(duì)象(this引用就指向這個(gè)新對(duì)象)
  3. 執(zhí)行構(gòu)造函數(shù)中 的代碼(為這個(gè)新處理添加屬性)
  4. 返回新對(duì)象

person1和person2都保存著Person的一個(gè)不同的實(shí)例。這兩個(gè)對(duì)象都有一個(gè)constructor(構(gòu)造函數(shù))屬性,該屬性指向Person。

console.log(person1.constructor === Person);        // true
console.log(person2.constructor === Person);        // true

對(duì)象的constructor屬性最初用來(lái)標(biāo)識(shí)對(duì)象類型的。但是,提到檢測(cè)對(duì)象類型,還是instanceof操作符可靠一些。我們這里創(chuàng)建的所有對(duì)象既是Object的實(shí)例,也是Person的實(shí)例,這一點(diǎn)通過(guò)instanceof操作符可以驗(yàn)證。

console.log(person1 instanceof Object);     // true
console.log(person1 instanceof Person);     // true
console.log(person2 instanceof Object);     // true
console.log(person2 instanceof Person);     // true

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

構(gòu)造函數(shù)與其他函數(shù)的唯一區(qū)別,就在于調(diào)用方式不同,不過(guò),構(gòu)造函數(shù)也是函數(shù),不存在定義構(gòu)造函數(shù)的特殊語(yǔ)法。任何函數(shù),只要通過(guò)new操作符來(lái)調(diào)用,那它就可以作為構(gòu)造函數(shù);而任何函數(shù),如果不經(jīng)過(guò)new操作符,那它和普通函數(shù)也沒(méi)有什么兩樣。

// constructor function
var person = new Person("Nicholas",29,"Software Engineer");
person.sayName();       // "Nicholas"

// normal function
Person("Greg",27,"Doctor");     // this引用指向window
window.sayName();       // "Greg"

// 在另一個(gè)對(duì)象的作用域調(diào)用
var o = new Object();
Person.call(o,"Kristen",25,"Nurse");
o.sayName();            // "Kristen"

2. 構(gòu)造函數(shù)的問(wèn)題

構(gòu)造函數(shù)模式雖然好用,但也并非沒(méi)有缺點(diǎn)。使用構(gòu)造函數(shù)的主要問(wèn)題,就是每個(gè)方法都要在每個(gè)實(shí)例上重新寫一遍。在前面的例子中,person1和person2都有一個(gè)名為sayName()的方法,但那兩個(gè)方法不是同一個(gè)Function的實(shí)例。他們不是同一個(gè)函數(shù)。ES中的函數(shù)是對(duì)象,因此沒(méi)定義一個(gè)函數(shù),也就是實(shí)例化了一個(gè)對(duì)象。從邏輯角度講,相當(dāng)于這樣:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("console.log(this.name)");      // 與聲明函數(shù)邏輯上等價(jià)
}

以這種方式創(chuàng)建函數(shù),會(huì)導(dǎo)致不同的作用域鏈和標(biāo)識(shí)符解析。

console.log(person1.sayName === person2.sayName);       // false

然而,創(chuàng)建兩個(gè)完成同樣任務(wù)的Function實(shí)例的確沒(méi)有必要;況且有this對(duì)象在,根本不用在執(zhí)行代碼前就把函數(shù)綁定到特定對(duì)象上面。可以通過(guò)把函數(shù)定義轉(zhuǎn)移到構(gòu)造函數(shù)外來(lái)解決這個(gè)問(wèn)題。

理解:因?yàn)閠his對(duì)象的存在,我么只需要定義一個(gè)方法(函數(shù))就夠了,通過(guò)指定this來(lái)解決這個(gè)問(wèn)題。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}
function sayName() {
    console.log(this.name);
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
console.log(person1.sayName === person2.sayName);       // true

person1和person2就共享了一個(gè)全局作用域sayName()函數(shù),但是新問(wèn)題又來(lái)了。
在全局作用域中定義的函數(shù)實(shí)際上只能被某個(gè)對(duì)象調(diào)用,這讓全局作用域有點(diǎn)名不副實(shí)。并且,如果需要定義很多方法,那么就要定義很多個(gè)全局函數(shù),那么這個(gè)自定義的引用類型就沒(méi)有絲毫封裝性可言。

原型模式

每一個(gè)函數(shù)都有一個(gè)prototype屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,而這個(gè)對(duì)象的用途包括可以由特定類型的所有實(shí)例共享的屬性和方法。

理解:在函數(shù)上的prototype對(duì)象上的屬性和方法,可以共享給函數(shù)的所有實(shí)例。

function Person() {};

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
}

var person1 = new Person();
person1.sayName();      // "Nicholas"

var person2 = new Person();
person2.sayName();      // "Nicholas"

console.log(person1.sayName === person2.sayName);       // true

1. 理解原型對(duì)象

無(wú)論什么時(shí)候,只要?jiǎng)?chuàng)建了一個(gè)新函數(shù),就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性,這個(gè)屬性指向函數(shù)的原型對(duì)象。默認(rèn)情況下,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)constructor(構(gòu)造函數(shù))屬性,這個(gè)屬性指向一個(gè)prototype屬性所在函數(shù)的指針。
創(chuàng)建了自定義的構(gòu)造函數(shù)之后,其原型對(duì)象默認(rèn)只會(huì)取得constructor屬性;至于其他方法,則都是從Object繼承而來(lái)的。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后,該實(shí)例的內(nèi)部將包含一個(gè)指針(內(nèi)部屬性),指向構(gòu)造函數(shù)的原型對(duì)象。ECMA-262第5版管這個(gè)指針叫[[Prototype]]。雖然在腳本中沒(méi)有標(biāo)準(zhǔn)的方式訪問(wèn)[[Prototype]],但Firefox,Safari,Chrome在每個(gè)對(duì)象上都支持一個(gè)屬性__proto__;而在其他實(shí)現(xiàn)中,這個(gè)屬性對(duì)腳本則是完全不可見(jiàn)的。

雖然在所有實(shí)現(xiàn)都無(wú)法訪問(wèn)到[[Prototype]],但可以通過(guò)isPrototypeOf()方法來(lái)確定對(duì)象之間是否存在這種關(guān)系。從本質(zhì)上將,如果[[Prototype]]指向調(diào)用Object.isPrototypeOf()方法的對(duì)象(Person.prototype),那么這個(gè)方法就會(huì)返回true。

console.log(Person.prototype.isPrototypeOf(person1));       // true
console.log(Person.prototype.isPrototypeOf(person2));       // true

ES5新增了一個(gè)新方法,叫Object.getPrototypeOf(),在所有支持的實(shí)現(xiàn)中,這個(gè)方法返回[[Prototype]]的值。

console.log(Object.getPrototypeOf(person1) === Person.prototype);   // true
console.log(Object.getPrototypeOf(person1).name);                   // "Nicholas"

這里的第一行代碼只是確定Object.getPrototypeOf()返回的對(duì)象實(shí)際就是這個(gè)對(duì)象的原型。第二行代碼取得了原型對(duì)象中name屬性的值,Nicholas。使用Object.getPrototypeOf()可以方便的取得一個(gè)對(duì)象的原型,而這在利用原型實(shí)現(xiàn)繼承的情況下是非常重要的。

每當(dāng)代碼讀取某個(gè)對(duì)象的某個(gè)屬性時(shí),都會(huì)執(zhí)行一次搜索,目標(biāo)是具有給定名字的屬性。搜索首先從對(duì)象實(shí)例開(kāi)始。如果在實(shí)例中找到了具有給定名字的屬性,則返回屬性的值;如果沒(méi)有找到,則繼續(xù)搜索指針指向的原型對(duì)象,在原型對(duì)象中查找具有給定名字的屬性,有,則返回屬性值。

雖然可以通過(guò)對(duì)象實(shí)例訪問(wèn)保存在原型中的值,但卻不能通過(guò)對(duì)象實(shí)例重寫原型中的值。如果我們?cè)趯?shí)例添加一個(gè)屬性,而該屬性與實(shí)例原型的一個(gè)屬性同名,那我們就在實(shí)例中創(chuàng)建該屬性,該屬性將會(huì)屏蔽原型中的那個(gè)屬性。

function Person() {};

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
}

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

person1.name = "Greg";
console.log(person1.name);      // Greg
console.log(person2.name);      // Nicholas

person1.name被一個(gè)新值給屏蔽了,因?yàn)閷?shí)例上本身存在一個(gè)自有屬性。

當(dāng)為對(duì)象實(shí)例添加一個(gè)屬性時(shí),這個(gè)屬性就會(huì)屏蔽原型對(duì)象中保存的同名屬性;換句話說(shuō),添加這個(gè)屬性會(huì)阻止我們?cè)L問(wèn)原型中的那個(gè)屬性,但不會(huì)修改屬性。即使將這個(gè)屬性設(shè)置為null,也只會(huì)在實(shí)例中設(shè)置這個(gè)屬性,而不會(huì)恢復(fù)其指向原型的鏈接。
不過(guò),使用delete操作符則可以完全刪除實(shí)例屬性,從而讓我們重新訪問(wèn)到原型屬性。

function Person() {};

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
}

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

person1.name = "Greg";
console.log(person1.name);      // "Greg"
console.log(person1.name);      // "Nicholas"

delete person1.name;
console.log(pesron1.name);      // "Nichoals"

使用hasOwnProperty()方法可以檢測(cè)一個(gè)屬性是存在于實(shí)例中,還是存在與原型中 。這個(gè)方法(不要忘了它也是從Object繼承來(lái)的)只在給定屬性存在于對(duì)象實(shí)例中時(shí),才會(huì)返回true。

function Person() {};

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
}

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

console.log(person1.hasOwnProperty("name"));        // false
person1.name = "Greg";                              // "Greg"   自有屬性
console.log(person1.name);                          // true

console.log(person2.name);                          // "Nicholas"   原型屬性
console.log(person2.hasOwnProperty("name"));        // false

delete person1.name;
console.log(person1.name);                          // "Nichoals"   原型屬性
console.log(person1.hasOwnProperty("name"));        // false

2. 原型與in操作符

有兩種方式使用in操作符

  • 單獨(dú)使用
  • 在for-in循環(huán)中使用

單獨(dú)使用時(shí),in操作符會(huì)在通過(guò)對(duì)象能夠訪問(wèn)給定屬性時(shí)返回true,無(wú)論自有屬性還是原型屬性。

function Person() {};

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
}

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

console.log(person1.hasOwnProperty("name"))     // false
console.log("name" in person1)                  // true

person1.name = "Greg";
console.log(person1.name);                      // "Greg"
console.log(person1.hasOwnProperty("name"));    // true
console.log("name" in person1);                 // true

console.log(person2.name);                      // "Nicholas"
console.log(person2.hasOwnProperty("name"));    // false
console.log("name" in person2);                 // true

delete person1.name;
console.log(person1.name);                      // "Nicholas"
console.log(person1.hasOwnProperty("name"));    // false
console.log("name" in person1);                 // true

我們可以根據(jù)這兩個(gè)方法寫一個(gè)判斷屬性是否在原型上的函數(shù):
hasPrototypeProperty()

function Person() {};

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
}

function hasPrototypeProperty(object,name) {
    return !object.hasOwnProperty(name) && (name in object);
}

var person = new Person();
console.log(hasPrototypeProperty(person,"name"));           // true

person.name = "Greg";
console.log(hasPrototypeProperty(person,"name"));           // false

在使用for-in循環(huán)時(shí),返回的是所有能夠通過(guò)對(duì)象訪問(wèn)、可枚舉(enumerable)的屬性,其中既包括存在實(shí)例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉屬性(即將[[Enumerable]]設(shè)置為false)的實(shí)例屬性也會(huì)在for-in循環(huán)中返回,因?yàn)楦鶕?jù)規(guī)定,所有開(kāi)發(fā)人員定義的屬性都是可枚舉的。

要取得對(duì)象上所有可枚舉的實(shí)例屬性,可以使用ES5的Object.keys()方法。這個(gè)方法接受一個(gè)對(duì)象作為參數(shù),返回一個(gè)包含所有可枚舉屬性的字符串?dāng)?shù)組。

function Person() {};

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
}


var keys = Object.keys(Person.prototype);
console.log(keys);              // ["name","age","job","sayName"];

var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
console.log(p1keys);            // ["name","age"]

Object.getOwnPropertyNames()

如果你想要得到所有實(shí)例屬性,無(wú)論它是否可枚舉,都可以使用Object.getOwnPropertyNames()方法。

function Person() {};

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
}


var keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys);  
/*
0:"constructor"
1:"name"
2:"age"
3:"job"
4:"sayName"
*/

3. 更簡(jiǎn)單的原型語(yǔ)法

沒(méi)添加一個(gè)屬性和方法都要敲一遍Person.prototype。為減少不必要的輸入,也為了從視覺(jué)上優(yōu)化封裝原型功能,更常見(jiàn)的做法是用一個(gè)包含所有屬性和方法的對(duì)象字面量來(lái)重寫整個(gè)原型對(duì)象。

function Person() {};

Person.prototype = {
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function () {
        console.log(this.name);
    }
}

Person.prototype設(shè)置為等于一個(gè)對(duì)象字面量形式創(chuàng)建的新對(duì)象。最終結(jié)果相同,但有一個(gè)例外,constructor屬性不再指向Person了。
原來(lái),每創(chuàng)建一個(gè)函數(shù),就會(huì)同時(shí)創(chuàng)建它的prototype對(duì)象,這個(gè)對(duì)象也會(huì)自動(dòng)獲得constructor屬性。而我們?cè)谶@里完全重寫了默認(rèn)的prototype對(duì)象,因此constructor屬性也就變成了新對(duì)象的constructor屬性(指向Object構(gòu)造函數(shù)),不在指向Person函數(shù)。此時(shí),盡管instanceo操作符還能返回正確的結(jié)果,但通過(guò)constructor已經(jīng)無(wú)法確定對(duì)象類型了。

var friend = new Person();

console.log(friend instanceof Object);          // true
console.log(friend instanceof Person);          // true
console.log(friend.constructor == Person);      // false
console.log(friend.constructor == Object);      // true

在此,用instanceof操作符測(cè)試Object和Person仍然返回true,但constructor屬性則等于Object而不等于Person了。如果constructor的值真的很重要,可以將它設(shè)置回適當(dāng)?shù)闹怠?/p>

function Person() {};

Person.prototype = {
    constructor: Person,
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function () {
        console.log(this.name);
    }
};

以上代碼特意包含了一個(gè)constructor屬性,并將它的值設(shè)置為Person,從而確保了通過(guò)該屬性能夠訪問(wèn)到適當(dāng)?shù)闹怠?br> 注意,以這種方式重設(shè)constructor屬性會(huì)導(dǎo)致它的[[Enumerable]]特性被設(shè)置為true。默認(rèn)情況下,原生的constructor屬性是不可枚舉的,因此如果使用兼容ES5的JS引擎,可以試一試Object.defineProperty()。

Object.defineProperty(Person.prototype, "constrcutor", {
    enumerable: false,
    value: Person
});

4. 原型的動(dòng)態(tài)性
由于在原型中查找值的過(guò)程是一次搜索,因此我們對(duì)原型對(duì)象所做的任何修改都能立刻從實(shí)例上反映出來(lái)——即使是先創(chuàng)建實(shí)例后修改原型也是如此。

var friend = new Person();
Person.prototype.sayHi = function () {
    console.log("Hi");
};

friend.sayHi();     // "Hi"     

實(shí)例與原型之間的松散鏈接關(guān)系,當(dāng)我們調(diào)用friend.sayHi(),首先會(huì)在實(shí)例中搜索名為sayHi的屬性,沒(méi)找到則像原型中查找,因?yàn)閷?shí)例與原型之間的鏈接不過(guò)是一個(gè)指針,而非一個(gè)副本,因此就可以在原型中找到新的sayHi屬性并返回引用在那里的函數(shù)。

盡管可以隨時(shí)為原型添加屬性和方法,并且修改能夠立即在所有對(duì)象實(shí)例中反映出來(lái),但如果是重寫整個(gè)原型對(duì)象,那么情況就不一樣了。
調(diào)用構(gòu)造函數(shù)會(huì)為實(shí)例添加一個(gè)指向最初原型的[[Prototype]]指針,而把原型修改為另一個(gè)對(duì)象就等于切斷了這個(gè)聯(lián)系。

function Person() {};

var friend = new Person();

Person.prototype = {
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function () {
        console.log(this.name);
    }
}

friend.sayName();       // TypeError: sayName is not a function

在這個(gè)例子中,先創(chuàng)建Person一個(gè)實(shí)例,然后又重寫了原型對(duì)象。然后調(diào)用sayName函數(shù)發(fā)生了錯(cuò)誤,因?yàn)閒riend指向的原型中不包含以該名字命名的屬性。

重寫原型對(duì)象,切斷了現(xiàn)有原型和任何之前已經(jīng)存在的對(duì)象實(shí)例之間的聯(lián)系;它們引用的仍然是最初的原型。

5. 原生對(duì)象的原型
原型模式的重要性不僅體現(xiàn)在創(chuàng)建自定義類型方面,就連所有原生的引用類型,都是采用這種模式創(chuàng)建的。例如,在Array.prototype中可以找到sort()方法,String.prototype中可以找到substring()方法。

console.log(typeof Array.prototype.sort);           // funciton
console.log(typeof String.prototype.substring);     // function

通過(guò)原生對(duì)象的原型,不僅可以取得所有默認(rèn)方法的引用,而且也可以定義新方法。可以像修改自定義對(duì)象的原型一樣修改原生對(duì)象的原型。


String.prototype.startsWidth = function(text) {
    return this.indexOf(text) === 0;
};

var msg = "Hello World";
console.log(msg.startsWith("Hello"));   // true

盡管可以這樣做,但我們不推薦在產(chǎn)品化的程序中修改原生對(duì)象原型,會(huì)導(dǎo)致嚴(yán)重的意外。

6. 原型對(duì)象的問(wèn)題

原型模式省略了為構(gòu)造函數(shù)傳遞初始化參數(shù)這一環(huán)節(jié),所有實(shí)例在默認(rèn)情況下都將取得相同的屬性值。但還不是最大的問(wèn)題。
原型中所有屬性是被很多實(shí)例共享的,這種共享對(duì)于函數(shù)非常合適。對(duì)于那些包含基本類型值的屬性倒也說(shuō)得過(guò)去,可以在實(shí)例上添加同名屬性,屏蔽原型對(duì)應(yīng)屬性。
但是,對(duì)于引用類型值的屬性來(lái)說(shuō),就有問(wèn)題了:

function Person() {};

Person.prototype = {
    constructor: Person,
    name: "Nichoals",
    age: 29,
    job: "Software Engineer",
    friends: ["friend.1","friend.2"],
    sayName: function() {
        console.log(this.name);
    }
};

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

person1.friends.push("friend.3");

console.log(person1.friends);   // Array(3) ["friend.1", "friend.2", "friend.3"]
console.log(person2.friends);   // // Array(3) ["friend.1", "friend.2", "friend.3"]
console.log(person1.friends === person1.friends);   // true

假如初衷是為了在所有實(shí)例中共享一個(gè)數(shù)組,那沒(méi)什么問(wèn)題,但是,實(shí)例一般都是要有屬于自己的全部屬性的。


組合使用構(gòu)造函數(shù)模式和原型模式

構(gòu)造函數(shù)用于定義實(shí)例屬性,原型模式用于定義方法和共享的屬性。
另外,這種混成模式還支持像構(gòu)造函數(shù)傳遞參數(shù)。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["friend.1","friend.2"];
}
Person.prototype = {
    constructor: Person,
    sayName: function() {
        console.log(this.name);
    }
};

var person1 = new Person("Nicholas",29,"Software Engineer");
var person2 = new Person("Greg","27",'Doctor');

person1.friends.push("friend.3");

console.log(person1.friends);           // Array(3) ["friend.1", "friend.2", "friend.3"]
console.log(person2.friends);           // Array(2) ["friend.1", "friend.2"]
console.log(person1.friends === person2.friends);   // false
console.log(person1.sayName === person2.sayName);   // true

這種構(gòu)造函數(shù)與原型混成的模式,是ES中使用最廣泛、認(rèn)同度最高的一種創(chuàng)建自定義類型的方法。可以說(shuō),用來(lái)定義引用類型的一種默認(rèn)模式。


動(dòng)態(tài)原型模式
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    if (typeof this.sayName != "function") {
        Person.prototype.sayName = function() {
            console.log(this.name);
        }
    }
}

var friend = new Person("Nicholas",29,"Software Engineer");
friend.sayName();      // "Nicholas"

在if語(yǔ)句中,只在sayName()方法不存在的情況下,才會(huì)將它添加到原型中。這段代碼只會(huì)在初次調(diào)用構(gòu)造函數(shù)時(shí)才會(huì)執(zhí)行。此后,原型完成初始化,不需要再做什么修改了。
不過(guò)要記住,這里對(duì)原型做的修改,會(huì)立即在所有實(shí)例中得到反映 。其中,if語(yǔ)句檢查的可以是初始化之后應(yīng)該存在的任何屬性或方法——不必用一大堆if語(yǔ)句檢查每個(gè)屬性和方法;只要檢查其中一個(gè)即可。

使用動(dòng)態(tài)原型模式時(shí),不能使用對(duì)象字面量重寫原型,這樣會(huì)切斷現(xiàn)有實(shí)例與新原型之間的聯(lián)系。


穩(wěn)妥構(gòu)造函數(shù)模式

Douglas Crockford 發(fā)明了JavaScript中的穩(wěn)妥對(duì)象(durable objects)這個(gè)慨念。所謂穩(wěn)妥對(duì)象,指的是沒(méi)有公共屬性,而且其方法也不引用this對(duì)象。穩(wěn)妥對(duì)象最適合在一些安全的環(huán)境(這些環(huán)境會(huì)禁止使用this和new),或者在防止數(shù)據(jù)被其他應(yīng)用程序改動(dòng)時(shí)使用。

function Person(name,age,job) {
    // 創(chuàng)建要返回的對(duì)象
    var o = new Object();
    // 可以在這里定義私有變量和函數(shù)

    // 添加方法
    o.sayName = function() {
        console.log(name);
    };

    // 返回對(duì)象
    return o;
}

在以這種模式創(chuàng)建的對(duì)象中,除了使用sayName()方法之外,沒(méi)有其他方法訪問(wèn)name的值。
可以這樣使用穩(wěn)妥的Person構(gòu)造函數(shù)。

var friend = Person("Nicholas",29,"Software Engineer");
friend.sayName();       // "Nicholas"

這樣,變量friend中保存的是一個(gè)穩(wěn)妥對(duì)象,而除了對(duì)象sayName()方法外,沒(méi)有別的方法可以訪問(wèn)數(shù)據(jù)成員。即使有其他代碼會(huì)給這個(gè)對(duì)象添加方法或數(shù)據(jù)成員,但也不可能有別的方法訪問(wèn)轉(zhuǎn)入到構(gòu)造函數(shù)中的原始數(shù)據(jù),很安全。


繼承

繼承是OO語(yǔ)言中的一個(gè)最為人津津樂(lè)道的概念。許多OO語(yǔ)言都支持兩種繼承方式:接口繼承和實(shí)現(xiàn)繼承。接口繼承值繼承方法簽名,實(shí)現(xiàn)繼承則繼承實(shí)際的方法。由于函數(shù)沒(méi)有簽名,在ES中無(wú)法實(shí)現(xiàn)接口繼承。ES只支持實(shí)現(xiàn)繼承,而且其實(shí)現(xiàn)繼承主要依靠原型鏈完成。


原型鏈

ES中描述了原型鏈的概念,并將原型鏈作為主要方法。基本思想是利用原型讓一個(gè)引用類型繼承另一個(gè)引用類型的屬性和方法。
每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象,原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針,而實(shí)例都包含一個(gè)指向原型對(duì)象的內(nèi)部指針。那么假如我們讓一些對(duì)象等于另一個(gè)類型的實(shí)例,結(jié)果會(huì)怎樣?顯然,此時(shí)的一些對(duì)象將包含一個(gè)指向另一個(gè)原型的指針,相應(yīng)地,另一個(gè)原型中也包含著一個(gè)指向另一個(gè)構(gòu)造函數(shù)的指針。假如另一個(gè)原型又是另一個(gè)類型的實(shí)例,那么上述關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條。

實(shí)現(xiàn)原型鏈的一中基本模式:

function SuperType() {
    this.property = true;
};
SuperType.prototype.getSuperValue = function() {
    return this.property;
};

function SubType() {
    this.subproperty = false;
};

// 繼承了SuperType
SubType.prototype = new SuperType();        // 確立繼承關(guān)系

SubType.prototype.getSubValue = function() {
    return this.subproperty;
};

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

上例中,定義了兩個(gè)類型:SuperType和SubType。每個(gè)類型分別有一個(gè)屬性和一個(gè)方法。它們的主要區(qū)別是SubType繼承了SuperType,而繼承是通過(guò)創(chuàng)建SuperType的實(shí)例,并將該實(shí)例賦給SubType.prototype實(shí)現(xiàn)的。
實(shí)現(xiàn)的本質(zhì)時(shí)重寫原型對(duì)象,代之以一個(gè)新類型的實(shí)例。換句話說(shuō),原來(lái)存在于SuperType實(shí)例中的所有屬性和方法,現(xiàn)在也存在于SubType.prototype中了。

上面代碼中,我們沒(méi)有使用SubType默認(rèn)提供的原型,而是給它換了一個(gè)新原型;這個(gè)新原型就是SuperType的一個(gè)實(shí)例。于是,新原型不僅具有作為一個(gè)SuperType的實(shí)例所擁有的全部屬性和方法,而且其內(nèi)部還有一個(gè)指針,指向了SuperType的原型。
要注意instance.constructor現(xiàn)在指向的是SuperType,這是因?yàn)樵瓉?lái)SubType.prototype中的constructor被重寫的 緣故。

通過(guò)實(shí)現(xiàn)原型鏈,本質(zhì)上擴(kuò)展了原型搜索機(jī)制

調(diào)用instance.getSuperValue(),1)搜索實(shí)例;2)搜索SubType.prototype;3)搜索SuperType.prototype。

1. 別忘記默認(rèn)的原型
事實(shí)上,前面例子的原型鏈還少一環(huán)。引用類型默認(rèn)繼承Object,而這個(gè)繼承也是通過(guò)原型鏈實(shí)現(xiàn)的。函數(shù)的默認(rèn)原型都是Object的實(shí)例,因此默認(rèn)原型都會(huì)包含一個(gè)內(nèi)部指針,指向Object.prototype。這也是所有自定義類型都會(huì)繼承toString(),valueOf()等默認(rèn)方法的原因。

完整的原型鏈所示:


一句話:SubType繼承了SuperType,而SuperType繼承了Object。


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

可以通過(guò)兩種方式確定原型和實(shí)例的關(guān)系。第一種是使用instanceof操作符。只要用這個(gè)操作符來(lái)測(cè)試實(shí)例與原型鏈中出現(xiàn)過(guò)的構(gòu)造函數(shù)。

console.log(instance instanceof SubType);
console.log(instance instanceof SuperType);
console.log(instance instanceof Object);

由于原型鏈的關(guān)系,測(cè)試這三個(gè)構(gòu)造函數(shù)的結(jié)果都返回了true;

第二種方式使用isPrototypeOf()。同樣,只要是原型鏈中出現(xiàn)過(guò)的原型,都可以說(shuō)是原型鏈所派生的實(shí)例的原型。因此也會(huì)返回true。

console.log(Object.prototype.isPrototypeOf(instance));      // true
console.log(SubType.prototype.isPrototypeOf(instance));     // true
console.log(SuperType.prototype.isPrototypeOf(instance));   // true

3. 謹(jǐn)慎的定義方法

子類型有時(shí)候需要覆蓋超類型中的某個(gè)方法,或者需要添加超類型中不存在的某個(gè)方法。但不管怎么樣,給原型添加方法的代碼一定要放在替換原型的語(yǔ)句之后。

function SuperType() {
    this.prototype = true;
}
SuperType.prototype.getSuperValue = function() {
    console.log(this.prototype);
};

function SubType() {
    this.subprototype = false;
}

// inherit
SubType.prototype = new SuperType();

// Add new method
SubType.prototype.getSubValue = function() {
    console.log()
}

// Rewrite SuperType inside method
SubType.prototype.getSuperValue = function() {
    console.log(false);
}

var instance = new SubType();
instance.getSuperValue();               // false

在 SubType的實(shí)例鏈接到SuperType后定義的兩個(gè)方法,第一個(gè)方法getSubValue()被添加到了SubType中。第二個(gè)方法getSuperValue()是原型鏈已經(jīng)存在的一個(gè)方法,但重寫這個(gè)方法將會(huì)屏蔽原來(lái)的那個(gè)方法。胡那句話說(shuō),SubType的實(shí)例調(diào)用getSuperValue()時(shí),調(diào)用的就是這個(gè)重新定義的方法;通過(guò)SuperType的實(shí)例調(diào)用getSuperValue()時(shí),還會(huì)繼續(xù)調(diào)用原來(lái)的那個(gè)方法。

還有一點(diǎn)需要注意,即在通過(guò)原型鏈實(shí)現(xiàn)繼承時(shí),不能使用對(duì)象字面量創(chuàng)建原型方法。因?yàn)檫@樣做會(huì)重寫原型鏈。

function SuperType() {
    this.property = true;
}
SuperType.prototype.getSuperValue = function() {
    console.log(this.property);
};

function SubType() {
    this.subproperty = false;
}

// 繼承SuperType的原型
SubType.prototype = new SuperType();

SubType.prototype = {
    getSubValue: function() {
        console.log(this.subproperty);
    },
    someOhterMethod: function() {
        console.log(false);
    }
}

var instance = new SubType();
instance.getsuper();            // Error!

以上代碼展示了剛剛SuperType的實(shí)例賦值給原型,緊接著又將原型替換成一個(gè)對(duì)象字面量而導(dǎo)致的問(wèn)題。由于現(xiàn)在的原型鏈包含的是一個(gè)Object的實(shí)例,而非SuperType的實(shí)例,因此我們?cè)O(shè)想中的原型鏈已經(jīng)被切斷——SuperType和SubType之間已經(jīng)沒(méi)有關(guān)系了。

4. 原型鏈的問(wèn)題
原型鏈雖然很強(qiáng)大,可以用它來(lái)實(shí)現(xiàn)繼承,但它也存在一些問(wèn)題。其中,最主要問(wèn)題來(lái)自包含引用類型值的原型。

function SuperType() {
    this.colors = ["red","blue","green"];
}
function SubType() {};

// 繼承了SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);      // Array(4) ["red", "blue", "green", "black"]

var instance2 = new SubType();
console.log(instance2.colors);      // Array(4) ["red", "blue", "green", "black"]

這個(gè)例子中的SuperType構(gòu)造函數(shù)定義了一個(gè)colors屬性,該屬性包含一個(gè)數(shù)組(引用類型)。SuperType的每個(gè)實(shí)例都會(huì)包含自己數(shù)組的colors屬性。當(dāng)SubType通過(guò)原型鏈繼承了SuperType之后,SubType.prototype就變成了SuperType的一個(gè)實(shí)例,因此它擁有了一個(gè)它自己的colors屬性。
但是,SubType的所有實(shí)例都會(huì)共享這一個(gè)colors屬性。而我們對(duì)instanceof1.colors的修改能夠通過(guò)instance2.colors反映出來(lái)
原型鏈的第二個(gè)問(wèn)題是:在創(chuàng)建子類型的實(shí)例時(shí),不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)。或者應(yīng)該說(shuō):是沒(méi)有辦法在不影響所有對(duì)象實(shí)例的情況下,給超類型的構(gòu)造函數(shù)傳遞參數(shù)
有鑒于此,實(shí)踐中很少會(huì)單獨(dú)使用原型鏈。


借用構(gòu)造函數(shù)

在解決原型中包含引用類型值所帶來(lái)問(wèn)題的過(guò)程中,開(kāi)發(fā)者開(kāi)始使用一種叫做借用構(gòu)造函數(shù)的技術(shù)(也叫偽造對(duì)象或者經(jīng)典繼承)。這種技術(shù)的基本思想相當(dāng)簡(jiǎn)單,即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù),別忘了,函數(shù)只不過(guò)是在特定環(huán)境中執(zhí)行代碼的對(duì)象,因此通過(guò)apply和call方法也可以在新創(chuàng)建的對(duì)象上執(zhí)行構(gòu)造函數(shù)。

function SuperType() {
    this.colors = ["red","blue","green"];
}
function SubType() {
    // 繼承了SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);      // ["red","blue","green","black"];

var instance2 = new SubType();
console.log(instance2.colors);      // ["red","blue","green"];

代碼中加粗的那一行代碼"借調(diào)"了超類型的構(gòu)造函數(shù),通過(guò)call()方法,我們實(shí)際上是在(未來(lái)將要)新創(chuàng)建的SubType實(shí)例的環(huán)境下調(diào)用了SuperType或者函數(shù)。這樣一來(lái),就會(huì)在新SubType對(duì)象上執(zhí)行SuperType()函數(shù)定義的所有對(duì)象初始化代碼。結(jié)果,SubType的每個(gè)實(shí)例就都會(huì)具有自己的colors屬性的副本了。

這里可能有一些反直覺(jué),以下改寫幫助理解:

function SuperType() {
    this.colors = ["red","blue","green"];
}
function SubType() {
    // 繼承了SuperType
    SuperType.call(this);
}

SuperType.call(window);
console.log(window.colors);     // Array(3) ["red", "blue", "green"]

SuperType.call(SubType);
console.log(SubType.colors);    // Array(3) ["red", "blue", "green"]

console.log(window.hasOwnProperty("colors"));     // true
console.log(SubType.hasOwnProperty("colors"));    // true

1. 傳遞參數(shù)
相對(duì)于原型鏈而言,借用構(gòu)造函數(shù)有一個(gè)很大優(yōu)勢(shì),即可以在子類型構(gòu)造函數(shù)中超類型構(gòu)造函數(shù)傳參。

function SuperType(name) {
    this.name = name;
}

function SubType() {
    // 繼承SuperType,同時(shí)傳遞參數(shù)
    SuperType.call(this,"Nicholas");
    
    // 實(shí)例屬性
    this.age = 29;
}

var instance = new SubType();
console.log(instance.name);     // "Nicholas"
console.log(instance.age);      // 29

2.借用構(gòu)造函數(shù)的問(wèn)題
如果僅僅是借用構(gòu)造函數(shù),那么也將無(wú)法避免構(gòu)造函數(shù)模式存在的問(wèn)題——方法都在構(gòu)造函數(shù)中定義,因此函數(shù)復(fù)用就無(wú)從談起了。而且,超類中原型定義的方法,對(duì)子類型而言也是不可見(jiàn)的,結(jié)果所有類型都只能使用構(gòu)造函數(shù)模式。


組合繼承

組合繼承,有時(shí)候也叫做偽經(jīng)典繼承,指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊,從而發(fā)揮二者之長(zhǎng)的一種繼承模式。其背后的思路是原型鏈實(shí)現(xiàn)對(duì)象原型屬性和方法的繼承,而通過(guò)借用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承。這樣,即通過(guò)在原型上定義方法實(shí)現(xiàn)了函數(shù)復(fù)用,又能夠保證每個(gè)實(shí)例都有自己的屬性。

function SuperType(name) {
    this.name = name;
    this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
}

function SubType(name,age) {
    // inherit
    SuperType.call(this,name);

    this.age = age;
}

// inherit method
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    console.log(this.age);
}


var instance1 = new SubType("Nicholas",29);
instance1.colors.push("black");

console.log(instance1.colors);  // Array(4) ["red", "blue", "green", "black"]
instance1.sayName();            // "Nicholas"
instance1.sayAge();             // 29

var instance2 = new SubType("Greg",27);
console.log(instance2.colors);  // Array(3) ["red", "blue", "green"]
instance2.sayName();            // "Greg"
instance2.sayAge();             // 27

這個(gè)例子中,SuperType構(gòu)造函數(shù)定義了兩個(gè)屬性:namecolorsSuperType的原型定義了一個(gè)方法sayName()SubType構(gòu)造函數(shù)在調(diào)用SuperType構(gòu)造函數(shù)時(shí)傳入了name參數(shù),緊接著又定義了方法sayAge()。這樣一來(lái),就可以讓兩個(gè)不同的SubType實(shí)例即分別擁有自己屬性———包括colors屬性,又可以使用相同的方法了。
組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了它們的優(yōu)點(diǎn),成為JS中最常用的繼承模式。而且instanceofisPrototypeOf()也能用于識(shí)別基于組合繼承創(chuàng)建的對(duì)象。


原型式繼承

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

在object()函數(shù)內(nèi)部,先創(chuàng)建一個(gè)臨時(shí)性的構(gòu)造函數(shù),然后將傳入的對(duì)象作為這個(gè)構(gòu)造函數(shù)的原型,最后返回了這個(gè)臨時(shí)類型的一個(gè)新實(shí)例。從本質(zhì)上講,object()對(duì)傳入其中的對(duì)象執(zhí)行了一次潛復(fù)制。

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

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);        // Array(5) ["Shelby", "Court", "Van", "Rob", "Barbie"]

原型式繼承,要求必須有一個(gè)對(duì)象可以作為另一個(gè)對(duì)象的基礎(chǔ)。如果有這么一個(gè)對(duì)象的話,可以把它傳遞給object()函數(shù),然后在根據(jù)具體需求對(duì)得到的對(duì)象加以修改即可。在這個(gè)例子中,可以作為另一個(gè)對(duì)象基礎(chǔ)的是person對(duì)象。于是將它傳入object函數(shù)中,然后該函數(shù)返回一個(gè)新對(duì)象。這個(gè)新對(duì)象將person作為原型,所以它的原型中就包含一個(gè)基本類型值屬性和一個(gè)引用類型值屬性。這意味著person.friends不僅屬于person所有,而且也會(huì)被anotherPerson以及yetAnotherPerson共享。
實(shí)際上,這相當(dāng)于又創(chuàng)建了person對(duì)象的兩個(gè)副本 。

ECMAScript5通過(guò)新增Object.create()方法規(guī)范化了原型式的繼承。這個(gè)方法接受兩個(gè)參數(shù):一個(gè)用作新對(duì)象原型的對(duì)象和(可選的)一個(gè)為新對(duì)象定義額外屬性的對(duì)象。在傳入一個(gè)參數(shù)的情況下,Object.create()與object()方法的行為相同。

var person = {
    name: "Nicholas",
    friends: ["Shelby","Court","Van"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);   // Array(5) ["Shelby", "Court", "Van", "Rob", "Barbie"]

Object.create()方法的第二個(gè)參數(shù)與Object.defineProperties()方法的第二個(gè)參數(shù)格式相同:每個(gè)屬性都是通過(guò)自己的描述符定義的。以這種方式指定的任何屬性都會(huì)覆蓋原型對(duì)象上的同名屬性,例如:

var person = {
    name: "Nicholas",
    friends: ["Shelby","Court","Van"]
};

var anotherPerson = Object.create(person,{
    name: {value: "Greg"}
});

console.log(anotherPerson.name);        // "Greg"

在沒(méi)有必要興師動(dòng)眾的創(chuàng)建構(gòu)造函數(shù),而只想讓一個(gè)對(duì)象與另一個(gè)對(duì)象保持類似的情況下,原型式繼承完全可以勝任的。
不過(guò)別忘了,包含引用類型值的屬性始終都會(huì)共享相應(yīng)的值,就像使用原型模式一樣。


寄生式繼承

寄生式繼承是與原型式繼承緊密相關(guān)的一種思路。寄生式繼承的思路與寄生構(gòu)造函數(shù)和工廠模式類似,即創(chuàng)建一個(gè)僅用于封裝繼承過(guò)程的函數(shù),該函數(shù)在內(nèi)部以某種方式來(lái)增強(qiáng)對(duì)象,最后在返回對(duì)象。

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

function createAnother(original) {
    var clone = object(original);       // 通過(guò)調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象
    clone.sayHi = function() {          // 以某種方式增強(qiáng)這個(gè)對(duì)象
        console.log("hi");
    };
    return clone;                       // 返回這個(gè)對(duì)象
}

在這個(gè)例子中,createAnother()函數(shù)接收了一個(gè)參數(shù),也就是將要作為新對(duì)象基礎(chǔ)的對(duì)象。然后,把這個(gè)對(duì)象(original)傳遞給object()函數(shù),將返回的結(jié)果賦值給clone。再為clone對(duì)象添加一個(gè)新方法sayHi(),最后返回clone對(duì)象。可以這樣使用createAnother()函數(shù):

var person = {
    name: "Nicholas",
    friends: ["Shelby","Court","Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();      // hi


繼承組合式繼承

組合繼承是JavaScript最常用的繼承模式,不過(guò)它也有自己的不足。組合繼承最大的問(wèn)題就是無(wú)論什么情況下,都會(huì)調(diào)用兩次超類型操作函數(shù):一次創(chuàng)建子類型原型時(shí)候,另一次在子類型構(gòu)造函數(shù)內(nèi)部。子類型最終會(huì)包含超類型對(duì)象的全部實(shí)例屬性,但我們不得不在在調(diào)用子類型構(gòu)造函數(shù)時(shí)重寫這些屬性。

function SuperType(name) {
    this.name = name;
    this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function() {console.log(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() {console.log(this.age)};

在第一次調(diào)用SuperType構(gòu)造函數(shù)時(shí),SubType.prototype會(huì)得到兩個(gè)屬性:name和colors;它們都是SuperType的實(shí)例屬性,只不過(guò)現(xiàn)在位于SubType的原型中。
當(dāng)調(diào)用SubType構(gòu)造函數(shù)時(shí),又會(huì)調(diào)用一次SuperType構(gòu)造函數(shù),這一次又在新對(duì)象上創(chuàng)建了實(shí)例屬性name和colors。于是,這兩個(gè)屬性就屏蔽了原型中兩個(gè)同名屬性。

過(guò)程如下圖:


有兩組name和colors屬性:一組在實(shí)例上,一組在SubType原型中,這就是調(diào)用兩次SuperType構(gòu)造函數(shù)的記過(guò)。解決這個(gè)問(wèn)題的方法——寄生組合式繼承。

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

寄生組合式繼承基本模式:

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

function ineritPrototype(subType,superType) {
    var prototype = object(superType.prototype);        // 創(chuàng)建對(duì)象
    prototype.constructor = subType;                    // 增強(qiáng)對(duì)象
    subType.prototype = prototype;                      // 指定對(duì)象
}

這個(gè)示例中的inheritPrototype()函數(shù)實(shí)現(xiàn)了寄生組合式繼承的最簡(jiǎn)單形式。這個(gè)函數(shù)接受兩個(gè)參數(shù):子類型構(gòu)造函數(shù)和超類型構(gòu)造函數(shù)。在函數(shù)內(nèi)部,第一步創(chuàng)建超類型原型的一個(gè)副本。第二步是為創(chuàng)建的副本添加constructor屬性,從而彌補(bǔ)因重寫原型而失去的默認(rèn)的constructor屬性。
最有一步,將新創(chuàng)建的對(duì)象(即副本)賦值給子類型的原型。這樣,就可以調(diào)用inheritPrototype()函數(shù)的語(yǔ)句,去替換前面例子中為子類型原型賦值的語(yǔ)句了。

function object(o) {
    function F() {};
    F.prototype = o;
    return new F();
}
function inheritPrototype(subType,superType) {
    var prototype = object(superType.prototype);        // 創(chuàng)建對(duì)象
    prototype.constructor = subType;                    // 增強(qiáng)對(duì)象
    subType.prototype = prototype;                      // 指定對(duì)象
}

function SuperType(name) {
    this.name = name;
    this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function() {console.log(this.name)};

function SubType(name,age) {
    SuperType.call(this,name);

    this.age = age;
}

inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function() {
    console.log(this.age);
}

這個(gè)例子的高效率體現(xiàn)在它只調(diào)用了一次SuperType構(gòu)造函數(shù),并且因此避免了在SubType.prototype上面創(chuàng)建不必要、多余的屬性。與此同時(shí),原型鏈還能保持不變;因此,還能夠正常使用instanceof和isPrototypeof()。

最后編輯于
?著作權(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ù)。

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