js高級程序設計(六)面向對象


ECMA-262 把對象定義為:“無序屬性的集合,其屬性可以包含基本值、對象或者函數。”嚴格來講,這就相當於說對象是一組沒有特定順序的值。對象的每個屬性或方法都有一個名字,而每個名字都映射到一個值。正因為這樣(以及其他將要討論的原因),我們可以把ECMAScript 的對象想象成散列表:無非就是一組名值對,其中值可以是數據或函數。
每個對象都是基於一個引用類型創建的,這個引用類型可以是第5 章討論的原生類型,也可以是開發人員定義的類型。

理解對象

創建自定義對象的最簡單方式就是創建一個Object 的實例,然后再為它添加屬性和方法

var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function(){
    alert(this.name);
};

用對象字面量語法可以寫成這樣:

var person = {
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function(){
      alert(this.name);
  }
};

屬性類型

ECMAScript 中有兩種屬性:數據屬性訪問器屬性

1數據屬性

[[Configurable]]:表示能否通過delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。像前面例子中那樣直接在對象上定義的屬性,它們的這個特性默認值為true。

[[Enumerable]]:表示能否通過for-in 循環返回屬性。像前面例子中那樣直接在對象上定義的屬性,它們的這個特性默認值為true。

[[Writable]]:表示能否修改屬性的值。像前面例子中那樣直接在對象上定義的屬性,它們的這個特性默認值為true。

[[Value]]:包含這個屬性的數據值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值保存在這個位置。這個特性的默認值為undefined。

對於像前面例子中那樣直接在對象上定義的屬性,它們的[[Configurable]]、[[Enumerable]]和[[Writable]]特性都被設置為true,而[[Value]]特性被設置為指定的值。

要修改屬性默認的特性,必須使用ECMAScript 5 的Object.defineProperty()方法。這個方法接收三個參數:屬性所在的對象、屬性的名字和一個描述符對象。其中,描述符(descriptor)對象的屬性必須是:configurable、enumerable、writable 和value。設置其中的一或多個值,可以修改對應的特性值。

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

這個例子創建了一個名為name 的屬性,它的值"Nicholas"是只讀的。這個屬性的值是不可修改的,如果嘗試為它指定新值,則在非嚴格模式下,賦值操作將被忽略;在嚴格模式下,賦值操作將會導致拋出錯誤。

類似的規則也適用於不可配置的屬性。

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

把configurable 設置為false,表示不能從對象中刪除屬性。

一旦把屬性定義為不可配置的,就不能再把它變回可配置了。此時,再調用Object.defineProperty()方法修改除writable 之外的特性,都會導致錯誤:

var person = {};
Object.defineProperty(person, "name", {
    configurable: false,
    value: "Nicholas"
});
//拋出錯誤
Object.defineProperty(person, "name", {
    configurable: true,
    value: "Nicholas"
});

可以多次調用Object.defineProperty()方法修改同一個屬性,但在把configurable特性設置為false 之后就會有限制了。

2訪問器屬性

訪問器屬性不包含數據值;它們包含一對兒getter 和setter 函數(不過,這兩個函數都不是必需的)。在讀取訪問器屬性時,會調用getter 函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用setter 函數並傳入新值,這個函數負責決定如何處理數據。訪問器屬性有如下4 個特性。

[[Configurable]]:表示能否通過delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為數據屬性。對於直接在對象上定義的屬性,這個特性的默認值為true。

[[Enumerable]]:表示能否通過for-in 循環返回屬性。對於直接在對象上定義的屬性,這個特性的默認值為true。

[[Get]]:在讀取屬性時調用的函數。默認值為undefined。

[[Set]]:在寫入屬性時調用的函數。默認值為undefined。

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

var book = {
    _year: 2004,
    edition: 1
};
Object.defineProperty(book, "year", {
    get: function(){
    return this._year;
},
    set: function(newValue){
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    }
});
book.year = 2005;
alert(book.edition); //2

以上代碼創建了一個book 對象,並給它定義兩個默認的屬性:_year 和edition。_year 前面的下划線是一種常用的記號,用於表示只能通過對象方法訪問的屬性。

不一定非要同時指定getter 和setter。只指定getter 意味着屬性是不能寫,嘗試寫入屬性會被忽略。在嚴格模式下,嘗試寫入只指定了getter 函數的屬性會拋出錯誤。類似地,只指定setter 函數的屬性也不能讀,否則在非嚴格模式下會返回undefined,而在嚴格模式下會拋出錯誤。

支持ECMAScript 5 的這個方法的瀏覽器有IE9+(IE8 只是部分實現)、Firefox 4+、Safari 5+、Opera12+ 和Chrome 。在這個方法之前, 要創建訪問器屬性, 一般都使用兩個非標准的方法:__defineGetter__()和__defineSetter__()。這兩個方法最初是由Firefox 引入的,后來Safari 3、Chrome 1 和Opera 9.5 也給出了相同的實現。使用這兩個遺留的方法,可以像下面這樣重寫前面的例子。

var book = {
    _year: 2004,
    edition: 1
};
//定義訪問器的舊有方法
book.__defineGetter__("year", function(){
    return this._year;
});
book.__defineSetter__("year", function(newValue){
    if (newValue > 2004) {
        this._year = newValue;
        this.edition += newValue - 2004;
    }
});
book.year = 2005;
alert(book.edition); //2

在不支持Object.defineProperty() 方法的瀏覽器中不能修改[[Configurable]] 和[[Enumerable]]。

定義多個屬性

ECMAScript 5 又定義了一個Object.defineProperties()方法。利用這個方法可以通過描述符一次定義多個屬性。這個方法接收兩個對象參數:第一個對象是要添加和修改其屬性的對象,第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應。

var book = {};
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;
            }
      }
  }
});

以上代碼在book 對象上定義了兩個數據屬性(_year 和edition)和一個訪問器屬性(year)。

支持Object.defineProperties()方法的瀏覽器有IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。

讀取屬性的特性

使用ECMAScript 5 的Object.getOwnPropertyDescriptor()方法,可以取得給定屬性的描述符。這個方法接收兩個參數:屬性所在的對象和要讀取其描述符的屬性名稱。返回值是一個對象,如果是訪問器屬性,這個對象的屬性有configurable、enumerable、get 和set;如果是數據屬性,這個對象的屬性有configurable、enumerable、writable 和value。

var book = {};
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");
alert(descriptor.value); //2004
alert(descriptor.configurable); //false
alert(typeof descriptor.get); //"undefined"

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

在JavaScript 中,可以針對任何對象——包括 DOM 和BOM 對象,使用Object.getOwnProperty-Descriptor()方法。支持這個方法的瀏覽器有IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。

創建對象

雖然Object 構造函數或對象字面量都可以用來創建單個對象,但這些方式有個明顯的缺點:使用同一個接口創建很多對象,會產生大量的重復代碼。

工廠模式

工廠模式是軟件工程領域一種廣為人知的設計模式,這種模式抽象了創建具體對象的過程。考慮到在ECMAScript 中無法創建類,開發人員就發明了一種函數,用函數來封裝以特定接口創建對象的細節。

function createPerson(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

工廠模式雖然解決了創建多個相似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)

構造函數模式

ECMAScript 中的構造函數可用來創建特定類型的對象。像Object 和Array 這樣的原生構造函數,在運行時會自動出現在執行環境中。此外,也可以創建自定義的構造函數,從而定義自定義對象類型的屬性和方法。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    };
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

Person()中的代碼除了與createPerson()中相同的部分外,還存在以下不同之處:

沒有顯式地創建對象;

直接將屬性和方法賦給了this 對象;

沒有return 語句。

按照慣例,構造函數始終都應該以一個大寫字母開頭,而非構造函數則應該以一個小寫字母開頭。這個做法借鑒自其他OO 語言,主要是為了區別於ECMAScript 中的其他函數。要創建Person 的新實例,必須使用new 操作符。以這種方式調用構造函數實際上會經歷以下4個步驟:

(1) 創建一個新對象;
(2) 將構造函數的作用域賦給新對象(因此this 就指向了這個新對象);
(3) 執行構造函數中的代碼(為這個新對象添加屬性);
(4) 返回新對象。

person1 和person2 分別保存着Person 的一個不同的實例。這兩個對象都有一個constructor(構造函數)屬性,該屬性指向Person

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

對象的constructor 屬性最初是用來標識對象類型的。但是,提到檢測對象類型,還是instanceof操作符要更可靠一些。我們在這個例子中創建的所有對象既是Object 的實例,同時也是Person的實例

alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true

創建自定義的構造函數意味着將來可以將它的實例標識為一種特定的類型;而這正是構造函數模式勝過工廠模式的地方。person1 和person2 之所以同時是Object 的實例,是因為所有對象均繼承自Object。

1將構造函數當作函數

構造函數與其他函數的唯一區別,就在於調用它們的方式不同。任何函數,只要通過new 操作符來調用,那它就可以作為構造函數;而任何函數,如果不通過new 操作符來調用,那它跟普通函數也不會有什么兩樣。

// 當作構造函數使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
// 作為普通函數調用
Person("Greg", 27, "Doctor"); // 添加到window
window.sayName(); //"Greg"
// 在另一個對象的作用域中調用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"

2構造函數的問題

使用構造函數的主要問題,就是每個方法都要在每個實例上重新創建一遍。person1 和person2 都有一個名為sayName()的方法,但那兩個方法不是同一個Function 的實例。ECMAScript 中的函數是對象,因此每定義一個函數,也就是實例化了一個對象。從邏輯角度講,此時的構造函數也可以這樣定義。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("alert(this.name)"); // 與聲明函數在邏輯上是等價的
}

每個Person 實例都包含一個不同的Function 實例(以顯示name 屬性)的本質。說明白些,以這種方式創建函數,會導致不同的作用域鏈和標識符解析,但
創建Function 新實例的機制仍然是相同的。因此,不同實例上的同名函數是不相等的

alert(person1.sayName == person2.sayName); //false

創建兩個完成同樣任務的Function 實例的確沒有必要;況且有this 對象在,根本不用在執行代碼前就把函數綁定到特定對象上面。因此,大可像下面這樣,通過把函數定義轉移到構造函數外部來解決這個問題。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}
function sayName(){
    alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

由於sayName 包含的是一個指向函數的指針,因此person1 和person2 對象就共享了在全局作用域中定義的同一個sayName()函數。

可是新問題又來了:在全局作用域中定義的函數實際上只能被某個對象調用,這讓全局作用域有點名不副實。而更讓人無法接受的是:如果對象需要定義很多方
法,那么就要定義很多個全局函數,於是我們這個自定義的引用類型就絲毫沒有封裝性可言了。

原型模式

我們創建的每個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true

但與構造函數模式不同的是,新對象的這些屬性和方法是由所有實例共享的。換句話說,person1 和person2 訪問的都是同一組屬性和同一個sayName()函數。

1理解原型對象

無論什么時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個prototype屬性,這個屬性指向函數的原型對象。在默認情況下,所有原型對象都會自動獲得一個constructor(構造函數)屬性,這個屬性包含一個指向prototype 屬性所在函數的指針。就拿前面的例子來說,Person.prototype. constructor 指向Person。而通過這個構造函數,我們還可繼續為原型對象添加其他屬性和方法。

當調用構造函數創建一個新實例后,該實例的內部將包含一個指針(內部屬性),指向構造函數的原型對象。ECMA-262 第5 版中管這個指針叫[[Prototype]]。雖然在腳本中沒有標准的方式訪問[[Prototype]],但Firefox、Safari 和Chrome 在每個對象上都支持一個屬性__proto__;

雖然在所有實現中都無法訪問到[[Prototype]],但可以通過isPrototypeOf()方法來確定對象之間是否存在這種關系。從本質上講,如果[[Prototype]]指向調用isPrototypeOf()方法的對象(Person.prototype),那么這個方法就返回true,

alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true

我們用原型對象的isPrototypeOf()方法測試了person1 和person2。因為它們內部都有一個指向Person.prototype 的指針,因此都返回了true。

ECMAScript 5 增加了一個新方法,叫Object.getPrototypeOf(),在所有支持的實現中,這個方法返回[[Prototype]]的值。

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

支持這個方法的瀏覽器有IE9+、Firefox 3.5+、Safari 5+、Opera 12+和Chrome。

每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具有給定名字的屬性。搜索首先從對象實例本身開始。如果在實例中找到了具有給定名字的屬性,則返回該屬性的值;如果沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性。如果在原型對象中找到了這個屬性,則返回該屬性的值。也就是說,在我們調用person1.sayName()的時候,會先后執行兩次搜索。首先,解析器會問:“實例person1 有sayName 屬性嗎?”答:“沒有。”然后,它繼續搜索,再問:“person1 的原型有sayName 屬性嗎?”答:“有。”於是,它就讀取那個保存在原型對象中的函數。

雖然可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例重寫原型中的值。如果我們在實例中添加了一個屬性,而該屬性與實例原型中的一個屬性同名,那我們就在實例中創建該屬性,該屬性將會屏蔽原型中的那個屬性。

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); //"Greg"——來自實例
alert(person2.name); //"Nicholas"——來自原型

當為對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的同名屬性;換句話說,添加這個屬性只會阻止我們訪問原型中的那個屬性,但不會修改那個屬性。即使將這個屬性設置為null,也只會在實例中設置這個屬性,而不會恢復其指向原型的連接。不過,使用delete 操作符則可以完全刪除實例屬性,從而讓我們能夠重新訪問原型中的屬性

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); //"Greg"——來自實例
alert(person2.name); //"Nicholas"——來自原型
delete person1.name;
alert(person1.name); //"Nicholas"——來自原型

使用hasOwnProperty()方法可以檢測一個屬性是存在於實例中,還是存在於原型中。這個方法(不要忘了它是從Object 繼承來的)只在給定屬性存在於對象實例中時,才會返回true。

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
person1.name = "Greg";
alert(person1.name); //"Greg"——來自實例
alert(person1.hasOwnProperty("name")); //true
alert(person2.name); //"Nicholas"——來自原型
alert(person2.hasOwnProperty("name")); //false
delete person1.name;
alert(person1.name); //"Nicholas"——來自原型
alert(person1.hasOwnProperty("name")); //false

通過使用hasOwnProperty()方法,什么時候訪問的是實例屬性,什么時候訪問的是原型屬性就一清二楚了。

2原型與in 操作符

有兩種方式使用in 操作符:單獨使用和在for-in 循環中使用。在單獨使用時,in 操作符會在通過對象能夠訪問給定屬性時返回true,無論該屬性存在於實例中還是原型中。

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty(
"name")); //false alert("name" in person1); //true
person1.name = "Greg"; alert(person1.name); //"Greg" ——來自實例 alert(person1.hasOwnProperty("name")); //true alert("name" in person1); //true
alert(person2.name); //"Nicholas" ——來自原型 alert(person2.hasOwnProperty("name")); //false alert("name" in person2); //true
delete person1.name; alert(person1.name); //"Nicholas" ——來自原型 alert(person1.hasOwnProperty("name")); //false alert("name" in person1); //true

同時使用hasOwnProperty()方法和in 操作符,就可以確定該屬性到底是存在於對象中,還是存在於原型中,如下所示。

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

上面定義的hasPrototypeProperty()可以確定屬性是原型中的屬性。

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var person = new Person();
alert(hasPrototypeProperty(person, "name")); //true
person.name = "Greg";
alert(hasPrototypeProperty(person, "name")); //false

在使用for-in 循環時,返回的是所有能夠通過對象訪問的、可枚舉的(enumerated)屬性,其中既包括存在於實例中的屬性,也包括存在於原型中的屬性。屏蔽了原型中不可枚舉屬性(即將[[Enumerable]]標記為false 的屬性)的實例屬性也會在for-in 循環中返回,因為根據規定,所有開發人員定義的屬性都是可枚舉的——只有在IE8 及更早版本中例外。

IE 早期版本的實現中存在一個bug,即屏蔽不可枚舉屬性的實例屬性不會出現在for-in 循環中。

var o = {
toString : function(){
      return "My Object";
  }
};
for (var prop in o){
    if (prop == "toString"){
        alert("Found toString"); //在IE 中不會顯示
    }
}

當以上代碼運行時,應該會顯示一個警告框,表明找到了toString()方法。這里的對象o 定義了一個名為toString()的方法,該方法屏蔽了原型中(不可枚舉)的toString()方法。在IE 中,由於其實現認為原型的toString()方法被打上了值為false 的[[Enumerable]]標記,因此應該跳過該屬性,結果我們就不會看到警告框。該bug 會影響默認不可枚舉的所有屬性和方法,包括:hasOwnProperty()、propertyIsEnumerable()、toLocaleString()、toString()和valueOf()。ECMAScript 5 也將constructor 和prototype 屬性的[[Enumerable]]特性設置為false,但並不是所有瀏覽器都照此實現。

要取得對象上所有可枚舉的實例屬性,可以使用ECMAScript 5 的Object.keys()方法。這個方法接收一個對象作為參數,返回一個包含所有可枚舉屬性的字符串數組。

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var keys = Object.keys(Person.prototype);
alert(keys); //"name,age,job,sayName"
var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); //"name,age"

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

var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //"constructor,name,age,job,sayName"

注意結果中包含了不可枚舉的constructor 屬性。Object.keys()和Object.getOwnProperty-Names()方法都可以用來替代for-in 循環。支持這兩個方法的瀏覽器有IE9+、Firefox 4+、Safari 5+、Opera12+和Chrome。

3更簡單的原型語法

更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象

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

最終結果相同,但有一個例外:constructor 屬性不再指向Person 了。前面曾經介紹過,每創建一個函數,就會同時創建它的prototype 對象,這個對象也會自動獲得constructor 屬性。而我們在這里使用的語法,本質上完全重寫了默認的prototype 對象,因此constructor 屬性也就變成了新對象的constructor 屬性(指向Object 構造函數),不再指向Person 函數。此時,盡管instanceof操作符還能返回正確的結果,但通過constructor 已經無法確定對象的類型了

var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true

如果constructor 的值真的很重要,可以像下面這樣特意將它設置回適當的值。

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

注意,以這種方式重設constructor 屬性會導致它的[[Enumerable]]特性被設置為true。默認情況下,原生的constructor 屬性是不可枚舉的,因此如果你使用兼容ECMAScript 5 的JavaScript 引擎,可以試一試Object.defineProperty()。

function Person(){
}
Person.prototype = {
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};
//重設構造函數,只適用於ECMAScript 5 兼容的瀏覽器
Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
});

4原型的動態性

由於在原型中查找值的過程是一次搜索,因此我們對原型對象所做的任何修改都能夠立即從實例上反映出來——即使是先創建了實例后修改原型也照樣如此。

var friend = new Person();
Person.prototype.sayHi = function(){
    alert("hi");
};
friend.sayHi(); //"hi"(沒有問題!)

其原因可以歸結為實例與原型之間的松散連接關系。當我們調用person.sayHi()時,首先會在實例中搜索名為sayHi 的屬性,在沒找到的情況下,會繼續搜索原型。因為實例與原型之間的連接只不過是一個指針,而非一個副本,因此就可以在原型中找到新的sayHi 屬性並返回保存在那里的函數。

如果是重寫整個原型對象,那么情況就不一樣了。我們知道,調用構造函數時會為實例添加一個指向最初原型的[[Prototype]]指針,而把原型修改為另外一個對象就等於切斷了構造函數與最初原型之間的聯系。請記住:實例中的指針僅指向原型,而不指向構造函數。

function Person(){
}
var friend = new Person();
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};
friend.sayName(); //error

5原生對象的原型

所有原生引用類型(Object、Array、String,等等)都在其構造函數的原型上定義了方法。在Array.prototype 中可以找到sort()方法,而在String.prototype 中可以找到substring()方法

alert(typeof Array.prototype.sort); //"function"
alert(typeof String.prototype.substring); //"function"

通過原生對象的原型,不僅可以取得所有默認方法的引用,而且也可以定義新方法。可以像修改自定義對象的原型一樣修改原生對象的原型,因此可以隨時添加方法。下面的代碼就給基本包裝類型String 添加了一個名為startsWith()的方法。

String.prototype.startsWith = function (text) {
    return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); //true

6原型對象的問題

原型中所有屬性是被很多實例共享的,這種共享對於函數非常合適。對於那些包含基本值的屬性倒也說得過去,畢竟(如前面的例子所示),通過在實例上添加一個同名屬性,可以隱藏原型中的對應屬性。然而,對於包含引用類型值的屬性來說,問題就比較突出了。

function Person(){
}
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    friends : ["Shelby", "Court"],
    sayName : function () {
        alert(this.name);
    }
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push(
"Van"); alert(person1.friends); //"Shelby,Court,Van" alert(person2.friends); //"Shelby,Court,Van" alert(person1.friends === person2.friends); //true

組合使用構造函數模式和原型模式

創建自定義類型的最常見方式,就是組合使用構造函數模式與原型模式。構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享的屬性。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}
Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

這種構造函數與原型混成的模式,是目前在ECMAScript 中使用最廣泛、認同度最高的一種創建自定義類型的方法。可以說,這是用來定義引用類型的一種默認模式。

動態原型模式

function Person(name, age, job){
  //屬性
    this.name = name;
    this.age = age;
    this.job = job;
    //方法
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        };
    }
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName()

寄生構造函數模式

在前述的幾種模式都不適用的情況下,可以使用寄生(parasitic)構造函數模式。

function Person(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"

除了使用new 操作符並把使用的包裝函數叫做構造函數之外,這個模式跟工廠模式其實是一模一樣的。構造函數在不返回值的情況下,默認會返回新對象實例。而通過在構造函數的末尾添加一個return 語句,可以重寫調用構造函數時返回的值。

這個模式可以在特殊的情況下用來為對象創建構造函數。假設我們想創建一個具有額外方法的特殊數組。由於不能直接修改Array 構造函數,因此可以使用這個模式。

function SpecialArray(){
    //創建數組
    var values = new Array();
    //添加值
    values.push.apply(values, arguments);
    //添加方法
    values.toPipedString = function(){
        return this.join("|");
    };
    //返回數組
    return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"

有一點需要說明:首先,返回的對象與構造函數或者與構造函數的原型屬性之間沒有關系;也就是說,構造函數返回的對象與在構造函數外部創建的對象沒有什么不同。為此,不能依賴instanceof 操作符來確定對象類型。由於存在上述問題,我們建議在可以使用其他模式的情況下,不要使用這種模式。

穩妥構造函數模式

道格拉斯·克羅克福德(Douglas Crockford)發明了JavaScript 中的穩妥對象(durable objects)這個概念。所謂穩妥對象,指的是沒有公共屬性,而且其方法也不引用this 的對象。穩妥對象最適合在一些安全的環境中(這些環境中會禁止使用this 和new),或者在防止數據被其他應用程序(如Mashup程序)改動時使用。穩妥構造函數遵循與寄生構造函數類似的模式,但有兩點不同:一是新創建對象的實例方法不引用this;二是不使用new 操作符調用構造函數。按照穩妥構造函數的要求,可以將前面的Person 構造函數重寫如下。

function Person(name, age, job){
    //創建要返回的對象
    var o = new Object();
    //可以在這里定義私有變量和函數
    //添加方法
    o.sayName = function(){
        alert(name);
    };
    //返回對象
    return o;
}

繼承

許多OO 語言都支持兩種繼承方式:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。如前所述,由於函數沒有簽名,在ECMAScript 中無法實現接口繼承。ECMAScript 只支持實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。

原型鏈

簡單回顧一下構造函數、原型和實例的關系:每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。那么,假如我們讓原型對象等於另一個類型的實例,結果會怎么樣呢?顯然,此時的原型對象將包含一個指向另一個原型的指針,相應地,另一個原型中也包含着一個指向另一個構造函數的指針。假如另一個原型又是另一個類型的實例,那么上述關系依然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂原型鏈的基本概念。

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

以上代碼定義了兩個類型:SuperType 和SubType。每個類型分別有一個屬性和一個方法。它們的主要區別是SubType 繼承了SuperType,而繼承是通過創建SuperType 的實例,並將該實例賦給SubType.prototype 實現的。實現的本質是重寫原型對象,代之以一個新類型的實例。

在上面的代碼中,我們沒有使用SubType 默認提供的原型,而是給它換了一個新原型;這個新原型就是SuperType 的實例。於是,新原型不僅具有作為一個SuperType 的實例所擁有的全部屬性和方法,而且其內部還有一個指針,指向了SuperType 的原型。最終結果就是這樣的:instance 指向SubType的原型, SubType 的原型又指向SuperType 的原型。getSuperValue() 方法仍然還在SuperType.prototype 中,但property 則位於SubType.prototype 中。這是因為property 是一個實例屬性,而getSuperValue()則是一個原型方法。既然SubType.prototype 現在是SuperType的實例,那么property 當然就位於該實例中了。此外,要注意SubType.prototype.constructor 現在指向的是SuperType,這是因為原來SubType.prototype 中的constructor 被重寫了的緣故。

當以讀取模式訪問一個實例屬性時,首先會在實例中搜索該屬性。如果沒有找到該屬性,則會繼續搜索實例的原型。在通過原型鏈實現繼承的情況下,搜索過程就得以沿着原型鏈繼續向上。

1別忘記默認的原型

所有引用類型默認都繼承了Object,而這個繼承也是通過原型鏈實現的。所有函數的默認原型都是Object 的實例,因此默認原型都會包含一個內部指針,指向Object.prototype。這也正是所有自定義類型都會繼承toString()、valueOf()等默認方法的根本原因。

一句話,SubType 繼承了SuperType,而SuperType 繼承了Object。當調用instance.toString()時,實際上調用的是保存在Object.prototype 中的那個方法。

2確定原型和實例的關系

可以通過兩種方式來確定原型和實例之間的關系。第一種方式是使用instanceof 操作符,只要用這個操作符來測試實例與原型鏈中出現過的構造函數,結果就會返回true。

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

由於原型鏈的關系,我們可以說instance 是Object、SuperType 或SubType 中任何一個類型的實例。因此,測試這三個構造函數的結果都返回了true。

第二種方式是使用isPrototypeOf()方法。同樣,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的實例的原型,因此isPrototypeOf()方法也會返回true

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

3謹慎地定義方法

子類型有時候需要重寫超類型中的某個方法,或者需要添加超類型中不存在的某個方法。但不管怎樣,給原型添加方法的代碼一定要放在替換原型的語句之后。

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}
//繼承了SuperType
SubType.prototype = new SuperType();
//添加新方法
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};
//重寫超類型中的方法
SubType.prototype.getSuperValue = function (){
    return false;
};
var instance = new SubType();
alert(instance.getSuperValue()); //false

第一個方法getSubValue()被添加到了SubType中。第二個方法getSuperValue()是原型鏈中已經存在的一個方法,但重寫這個方法將會屏蔽原來的那個方法。換句話說,當通過SubType 的實例調用getSuperValue()時,調用的就是這個重新定義的方法;但通過SuperType 的實例調用getSuperValue()時,還會繼續調用原來的那個方法。這里要格外注意的是,必須在用SuperType 的實例替換原型之后,再定義這兩個方法。

還有一點需要提醒讀者,即在通過原型鏈實現繼承時,不能使用對象字面量創建原型方法。因為這樣做就會重寫原型鏈

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

//使用字面量添加新方法,會導致上一行代碼無效
SubType.prototype = {
    getSubValue : function (){
        return this.subproperty;
    },
    someOtherMethod : function (){
        return false;
    }
};
var instance = new SubType();
alert(instance.getSuperValue()); //error!

由於現在的原型包含的是一個Object 的實例,而非SuperType 的實例,因此我們設想中的原型鏈已經被切斷——SubType 和SuperType 之間已經沒有關系了。

4原型鏈的問題

其中,最主要的問題來自包含引用類型值的原型。想必大家還記得,我們前面介紹過包含引用類型值的原型屬性會被所有實例共享;而這也正是為什么要在構造函數中,而不是在原型對象中定義屬性的原因。在通過原型來實現繼承時,原型實際上會變成另一個類型的實例。於是,原先的實例屬性也就順理成章地變成了現在的原型屬性了。

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

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

instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"

原型鏈的第二個問題是:在創建子類型的實例時,不能向超類型的構造函數中傳遞參數。實際上,應該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。有鑒於此,再加上前面剛剛討論過的由於原型中包含引用類型值所帶來的問題,實踐中很少會單獨使用原型鏈。

借用構造函數

在解決原型中包含引用類型值所帶來問題的過程中,開發人員開始使用一種叫做借用構造函數(constructor stealing)的技術(有時候也叫做偽造對象或經典繼承)。這種技術的基本思想相當簡單,即在子類型構造函數的內部調用超類型構造函數。別忘了,函數只不過是在特定環境中執行代碼的對象,因此通過使用apply()和call()方法也可以在(將來)新創建的對象上執行構造函數

function SuperType(){
    this.colors = ["red", "blue", "green"];
}
function SubType(){
    //繼承了SuperType
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

代碼中加粗的那一行代碼“借調”了超類型的構造函數。通過使用call()方法(或apply()方法也可以),我們實際上是在(未來將要)新創建的SubType 實例的環境下調用了SuperType 構造函數。這樣一來,就會在新SubType 對象上執行SuperType()函數中定義的所有對象初始化代碼。結果,SubType 的每個實例就都會具有自己的colors 屬性的副本了。

1傳遞參數

相對於原型鏈而言,借用構造函數有一個很大的優勢,即可以在子類型構造函數中向超類型構造函數傳遞參數。

function SuperType(name){
    this.name = name;
}
function SubType(){
    //繼承了SuperType,同時還傳遞了參數
    SuperType.call(this, "Nicholas");
    //實例屬性
    this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29

以上代碼中的SuperType 只接受一個參數name,該參數會直接賦給一個屬性。在SubType 構造函數內部調用SuperType 構造函數時,實際上是為SubType 的實例設置了name 屬性。為了確保SuperType 構造函數不會重寫子類型的屬性,可以在調用超類型構造函數后,再添加應該在子類型中定義的屬性。

2借用構造函數的問題

如果僅僅是借用構造函數,那么也將無法避免構造函數模式存在的問題——方法都在構造函數中定義,因此函數復用就無從談起了。而且,在超類型的原型中定義的方法,對子類型而言也是不可見的,結果所有類型都只能使用構造函數模式。考慮到這些問題,借用構造函數的技術也是很少單獨使用的。

組合繼承

組合繼承(combination inheritance),有時候也叫做偽經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮二者之長的一種繼承模式。其背后的思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數復用,又能夠保證每個實例都有它自己的屬性。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(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(){
    alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

在這個例子中,SuperType 構造函數定義了兩個屬性:name 和colors。SuperType 的原型定義了一個方法sayName()。SubType 構造函數在調用SuperType 構造函數時傳入了name 參數,緊接着又定義了它自己的屬性age。然后,將SuperType 的實例賦值給SubType 的原型,然后又在該新原型上定義了方法sayAge()。這樣一來,就可以讓兩個不同的SubType 實例既分別擁有自己屬性——包括colors 屬性,又可以使用相同的方法了。

組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成為JavaScript 中最常用的繼承模式。而且,instanceof 和isPrototypeOf()也能夠用於識別基於組合繼承創建的對象。

原型式繼承

道格拉斯·克羅克福德在2006 年寫了一篇文章,題為Prototypal Inheritance in JavaScript (JavaScript中的原型式繼承)。在這篇文章中,他介紹了一種實現繼承的方法,這種方法並沒有使用嚴格意義上的構造函數。他的想法是借助原型可以基於已有的對象創建新對象,同時還不必因此創建自定義類型。

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

在object()函數內部,先創建了一個臨時性的構造函數,然后將傳入的對象作為這個構造函數的原型,最后返回了這個臨時類型的一個新實例。從本質上講,object()對傳入其中的對象執行了一次淺復制。

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");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

克羅克福德主張的這種原型式繼承,要求你必須有一個對象可以作為另一個對象的基礎。如果有這么一個對象的話,可以把它傳遞給object()函數,然后再根據具體需求對得到的對象加以修改即可。在這個例子中,可以作為另一個對象基礎的是person 對象,於是我們把它傳入到object()函數中,然后該函數就會返回一個新對象。這個新對象將person 作為原型,所以它的原型中就包含一個基本類型值屬性和一個引用類型值屬性。這意味着person.friends 不僅屬於person 所有,而且也會被anotherPerson以及yetAnotherPerson 共享。實際上,這就相當於又創建了person 對象的兩個副本。

ECMAScript 5 通過新增Object.create()方法規范化了原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象。在傳入一個參數的情況下,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");
alert(person.friends);
//"Shelby,Court,Van,Rob,Barbie"

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

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});
alert(anotherPerson.name); //"Greg"

支持Object.create()方法的瀏覽器有IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。
在沒有必要興師動眾地創建構造函數,而只想讓一個對象與另一個對象保持類似的情況下,原型式繼承是完全可以勝任的。不過別忘了,包含引用類型值的屬性始終都會共享相應的值,就像使用原型模式一樣。

寄生式繼承

寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后再像真地是它做了所有工作一樣返回對象。

function createAnother(original){
    var clone = object(original); //通過調用函數創建一個新對象
    clone.sayHi = function(){ //以某種方式來增強這個對象
        alert("hi");
    };
return clone; //返回這個對象
}
var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

寄生組合式繼承

組合繼承是JavaScript 最常用的繼承模式;不過,它也有自己的不足。組合繼承最大的問題就是無論什么情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。沒錯,子類型最終會包含超類型對象的全部實例屬性,但我們不得不在調用子類型構造函數時重寫這些屬性。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name, age){
    SuperType.call(this, name); //第二次調用SuperType()
    this.age = age;
}
SubType.prototype = new SuperType(); //第一次調用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};

在第一次調用SuperType 構造函數時,SubType.prototype 會得到兩個屬性:name 和colors;它們都是SuperType 的實例屬性,只不過現在位於SubType 的原型中。當調用SubType 構造函數時,又會調用一次SuperType 構造函數,這一次又在新對象上創建了實例屬性name 和colors。於是,這兩個屬性就屏蔽了原型中的兩個同名屬性。

有兩組name 和colors 屬性:一組在實例上,一組在SubType 原型中。這就是調用兩次SuperType 構造函數的結果。好在我們已經找到了解決這個問題方法——寄生組合式繼承。

所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。其背后的基本思路是:不必為了指定子類型的原型而調用超類型的構造函數,我們所需要的無非就是超類型原型的一個副本而已。本質上,就是使用寄生式繼承來繼承超類型的原型,然后再將結果指定給子類型的原型。

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype); //創建對象
    prototype.constructor = subType; //增強對象
    subType.prototype = prototype; //指定對象
}

這個函數接收兩個參數:子類型構造函數和超類型構造函數。在函數內部,第一步是創建超類型原型的一個副本。第二步是為創建的副本添加constructor 屬性,從而彌補因重寫原型而失去的默認的constructor 屬性。最后一步,將新創建的對象(即副本)賦值給子類型的原型。這樣,我們就可以用調用inherit-Prototype()函數的語句,去替換前面例子中為子類型原型賦值的語句了

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name, age){
    SuperType.call(this, name);
    this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
    alert(this.age);
};

這個例子的高效率體現在它只調用了一次SuperType 構造函數,並且因此避免了在SubType.prototype 上面創建不必要的、多余的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用instanceof 和isPrototypeOf()。開發人員普遍認為寄生組合式繼承是引用類型最理想的繼承范式。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM