摘要:第章將詳細(xì)討論瀏覽器對(duì)象模型將構(gòu)造函數(shù)當(dāng)作函數(shù)構(gòu)造函數(shù)與其他函數(shù)的唯一區(qū)別就在于調(diào)用方式不同。而在構(gòu)造函數(shù)內(nèi)部,我們將屬性設(shè)置成等于全局的函數(shù)。默認(rèn)情況下,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)構(gòu)造函數(shù)屬性,這個(gè)屬性是一個(gè)指向?qū)傩运诤瘮?shù)的指針。
面向?qū)ο螅∣bject-Oriented, OO)的語言有一個(gè)標(biāo)志,它們都有類的概念,而通過類可以創(chuàng)建任意多個(gè)具有相同屬性和方法的對(duì)象。
ECMAScript中沒有類的概念,因此它的對(duì)象也與基類的語言中的對(duì)象有所不同
ECMA-262把對(duì)象定義為:“無序?qū)傩缘募?,其屬性可以包含基本值、?duì)象或者函數(shù)?!?我們可以把ECMAScript的對(duì)象想象成散列表:無非就是一組名值對(duì),其中值可以是數(shù)據(jù)或者函數(shù)
每個(gè)對(duì)象都是基于一個(gè)引用類型創(chuàng)建的,這個(gè)引用類型可以是第5章討論的原生類型,也可以是開發(fā)者定義的類型
理解對(duì)象// 創(chuàng)建對(duì)象,賦給屬性 var person = new Object(); person.name = "Nicholas"; person.age = 29; person.job = "Sofware Engineer"; person.sayName = function() { alert(this.name); } // 字面量方式創(chuàng)建對(duì)象 var person = { name: "Nicholas", age: 29, job: "Sofware Engineer", sayName: function() { alert(this.name); } }屬性類型
ECMAScript中有兩種屬性:數(shù)據(jù)屬性和訪問器屬性
數(shù)據(jù)屬性
數(shù)據(jù)屬性包含一個(gè)數(shù)值的位置。在這個(gè)位置可以讀取和寫入值。數(shù)據(jù)屬性有4個(gè)描述其行為的特性
[[Configurable]] 表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。像前述例子中那樣直接在對(duì)象定義的屬性,它們這個(gè)特性默認(rèn)值為true
[[Enumerable]] 表示能否通過for-in循環(huán)返回屬性。前述例子這個(gè)特性默認(rèn)為true
[[Writable]] 表示能否修改屬性的值。前述例子這個(gè)特性默認(rèn)為true
[[Value]] 包含這個(gè)屬性的數(shù)據(jù)值。讀取屬性值得實(shí)惠,從這個(gè)位置讀;寫入屬性值得實(shí)惠,把新值保存在這個(gè)位置。這個(gè)特性的默認(rèn)值為undefined
要修改屬性默認(rèn)的特性,必須使用ECMAScript5的Object.defineProperty()方法。
主要接收三個(gè)參數(shù):屬性所在的對(duì)象,屬性的名字和一個(gè)描述符對(duì)象。描述符(descriptor)對(duì)象的屬性必須是:configurable,enumerable,writalbe和value。
設(shè)置其中的一或多個(gè)值,可以修改對(duì)應(yīng)的特性值
var person = {}; // 創(chuàng)建了一個(gè)name屬性,它的值"Nicholas"是只讀的。 Object.defineProperty(person, "name", { writable: false, value: "Nicholas" }); console.log(person.name); // "Nicholas" // 在非嚴(yán)格模式下,賦值操作會(huì)被忽略 // 而在嚴(yán)格模式下,會(huì)導(dǎo)致錯(cuò)誤 person.name = "Greg"; console.log(person.name); // "Nicholas"
把configurable設(shè)置為false,表示不能從對(duì)象中刪除屬性。如果對(duì)這個(gè)屬性調(diào)用delete,則在非嚴(yán)格模式下什么也不會(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", { configurable: true, value: "Nicholas" })
可以多次調(diào)用Object.defineProperty()方法修改同一個(gè)屬性,但在把configurable特性設(shè)置為false之后就會(huì)有限制了。
調(diào)用Object.defineProperty()如果不指定,configurable,enumerable,writalbe 特性的默認(rèn)值都是false。多數(shù)情況下,沒有必要利用Object.defineProperty()方法提供的高級(jí)功能。
訪問器屬性訪問器屬性不包含數(shù)據(jù)值,它們包含一對(duì)getter和setter函數(shù)(不是必須的)
在讀取訪問器屬性時(shí),會(huì)調(diào)用getter函數(shù),這個(gè)函數(shù)負(fù)責(zé)返回有效的值
寫入訪問器屬性時(shí),會(huì)調(diào)用setter函數(shù)并傳入新值,這個(gè)函數(shù)負(fù)責(zé)決定如何處理數(shù)據(jù)
訪問器屬性有如下4個(gè)特性:
[[Configurable]] 表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為數(shù)據(jù)屬性。對(duì)于直接在對(duì)象上定義的屬性,這個(gè)特性的默認(rèn)值為true
[[Enumerable]] 表示能否通過for-in循環(huán)返回屬性。對(duì)于直接在對(duì)象上定義的屬性,這個(gè)特性的默認(rèn)值為true
[[Get]] 在讀取屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值為undefined
[[Set]] 在寫入屬性時(shí)調(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 (newValue) { if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } }); // 這是使用訪問器屬性的常見方式,即設(shè)置一個(gè)屬性的值會(huì)導(dǎo)致其他屬性發(fā)生變化。 book.year = 2005; console.log(book.edition); // 2
不一定非要同時(shí)制定getter和setter。只指定getter意味著屬性時(shí)不能寫,嘗試寫入屬性會(huì)被忽略。在嚴(yán)格模式下嘗試寫入只指定了getter函數(shù)的屬性會(huì)拋出錯(cuò)誤。
只指定setter意味著屬性時(shí)也不能讀,否則在非嚴(yán)格模式下回返回undefined,而在嚴(yán)格模式下會(huì)拋出錯(cuò)誤
支持ECMAScript 5這個(gè)方法的瀏覽器有IE9+(IE8只是部分實(shí)現(xiàn)),F(xiàn)irefox 4+, Safari 5+, Opera 12+, Chrome
在不支持Object.defineProperty()方法的瀏覽器中不能修改[[Configurable]] 和 [[Enumerable]]
定義多個(gè)屬性Object.defineProperties() 定義多個(gè)屬性的方法
var book = {} 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.editio += newValue - 2004; } } } })讀取屬性的特性
Ojbect.getOwnPropertyDescriptor() 方法,可以取得給定屬性的描述符,這個(gè)方法接收兩個(gè)參數(shù):屬性所在的對(duì)象和要讀取屬性描述符的屬性名稱。返回值時(shí)一個(gè)對(duì)象,如果是訪問器屬性,這個(gè)對(duì)象的屬性有configurable, enumerable, get, set;如果是數(shù)據(jù)屬性,這個(gè)對(duì)象的屬性有configurable, enumerable, writable, value
var book = {}; 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.editio += newValue - 2004; } } } }); var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); console.log(descriptor.value); // 2004 console.log(descriptor.configurable); // false 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"創(chuàng)建對(duì)象
雖然Object構(gòu)造函數(shù)或?qū)ο笞置媪慷伎梢杂脕韯?chuàng)建單個(gè)對(duì)象,但這些方式有個(gè)明顯的缺點(diǎn):使用同一個(gè)接口創(chuàng)建很多對(duì)象,會(huì)產(chǎn)生大量的重復(fù)代碼。為解決這個(gè)問題,人們開始使用工廠模式的一種變體
工廠模式考慮到ECMAScript中無法創(chuàng)建類,開發(fā)者就發(fā)明了一種函數(shù),用函數(shù)來封裝以特定接口創(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 () { alert(this.name); }; return o; } var person1 = createPerson("Nicholas", 29, "Software Engineer"); var person2 = createPerson("Greg", 27, "Doctor");
雖然工廠模式解決了創(chuàng)建多個(gè)相似對(duì)象的問題,但卻沒有解決對(duì)象識(shí)別的問題(即怎樣知道一個(gè)對(duì)象的類型)。
構(gòu)造函數(shù)模式可創(chuàng)建自定義的構(gòu)造函數(shù),從而定義自定義對(duì)象類型的屬性和方法
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");
構(gòu)造函數(shù)模式對(duì)比工廠模式存在以下不同之處
沒有顯式的創(chuàng)造對(duì)象
直接將屬性和方法賦給了this對(duì)象
沒有return 語句
函數(shù)名首字母大寫,按照慣例,構(gòu)造函數(shù)都應(yīng)該以一個(gè)大寫字母開頭,而非構(gòu)造函數(shù)小寫
person1 和 person2 分別保存著Person的一個(gè)不同的實(shí)例。兩者都有一個(gè)constructor(構(gòu)造函數(shù))屬性,改屬性指向Person
console.log(person1.constructor == Person); // true console.log(person2.constructor == Person); // true
對(duì)象的constructor屬性最初是用來標(biāo)識(shí)對(duì)象類型的。但是,檢測(cè)對(duì)象類型,還是instanceof操作符要更可靠。
這個(gè)例子中創(chuàng)建的所有對(duì)象既是Object的實(shí)例,同時(shí)也是Person的實(shí)例
console.log(person1 instanceof Object); // true console.log(person2 instanceof Object); // true console.log(person1 instanceof Person); // true console.log(person2 instanceof Person); // true
創(chuàng)建自定義的構(gòu)造函數(shù)意味著將來可以將它的實(shí)例標(biāo)識(shí)為一種特定的類型,這正是構(gòu)造韓式模式勝過工廠模式的地方。這個(gè)例子中,person1, person2 之所以同時(shí)是Object的實(shí)例,是因?yàn)樗袑?duì)象均繼承自O(shè)bject
以這種方式定義的構(gòu)造是定義在Global對(duì)象(瀏覽器中是window)中的。第8章將詳細(xì)討論瀏覽器對(duì)象模型(BOM)
將構(gòu)造函數(shù)當(dāng)作函數(shù)構(gòu)造函數(shù)與其他函數(shù)的唯一區(qū)別就在于調(diào)用方式不同。
構(gòu)造函數(shù)也是函數(shù),不存在定義構(gòu)造函數(shù)的特殊語法
任何函數(shù)只要通過new操作符來調(diào)用,那它就可以作為構(gòu)造函數(shù)
而任何函數(shù)不通過new操作符來調(diào)用,那它跟普通函數(shù)沒有區(qū)別
// 作為構(gòu)造函數(shù)使用 var person = new Person("Nicholas", 29, "Software Engineer"); person.sayName(); // "Nicholas" // 作為普通函數(shù)調(diào)用,此時(shí)this指向window對(duì)象 Person("Greg", 27, "Doctor"); // 添加到window對(duì)象上 window.sayName(); // "Greg" // 在另一個(gè)對(duì)象的作用域中調(diào)用 var o = new Object(); Person.call(o, "Kristen", 25, "Nurse"); o.sayName(); // "Kristen"構(gòu)造函數(shù)的問題
構(gòu)造函數(shù)模式的并非沒有缺點(diǎn)。主要問題就是每個(gè)方法都要在每個(gè)實(shí)例上重新創(chuàng)建一遍。在前述例子中person1和person2都有一個(gè)名為sayName()的方法,但那兩個(gè)方法不是同一個(gè)Function的實(shí)例。不要忘了ECMAScript中函數(shù)是對(duì)象,因此每定義一個(gè)函數(shù),也就是實(shí)例化了一個(gè)對(duì)象。從邏輯角度講,此時(shí)的構(gòu)造函數(shù)也可以這樣定義
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; // 與聲明函數(shù)再路基上是等價(jià)的 this.sayName = new Function("alert(this.name)"); } console.log(person1.sayName == person2.sayName); // false
有this對(duì)象在,沒有必要在執(zhí)行代碼前就把函數(shù)綁定到特定對(duì)象上面,通過把函數(shù)定義轉(zhuǎn)移到函數(shù)外部來簡化
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函數(shù)的定義轉(zhuǎn)移到了函數(shù)外部。而在構(gòu)造函數(shù)內(nèi)部,我們將sayName屬性設(shè)置成等于全局的sayName函數(shù)。如此一來,由于sayName包含的是一個(gè)指向函數(shù)的指針,因此person1和person2對(duì)象就共享了全局作用域中定義的同一個(gè)sayName() 函數(shù)。
新的問題出現(xiàn)了,在全局作用域中定義的函數(shù)實(shí)際上只能被某個(gè)對(duì)象調(diào)用,這讓全局作用域有點(diǎn)名不副實(shí)。而更讓人無法接受的是:如果對(duì)象需要定義很多方法,那么就要定義很多個(gè)全局函數(shù),于是我們這個(gè)自定義的引用類型就絲毫沒有封裝性可言了。
好在可以通過原型模式來解決
原型模式我們創(chuàng)建的每個(gè)函數(shù)都有一個(gè)prototype(原型),這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,而這個(gè)對(duì)象的用途是包含可以由特定類型的所有實(shí)例共享的屬性和方法。
prototype就是通過調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個(gè)對(duì)象實(shí)例的原型對(duì)象,使用原型對(duì)象的好處是可以讓所有對(duì)象實(shí)例共享它所包含的屬性和方法。換言之,不必再構(gòu)造函數(shù)中定義對(duì)象實(shí)例的信息,而是可以將這些信息直接添加到原型對(duì)象中
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" console.log(person1.sayName == person2.sayName); // true
在此,我們將sayName()方法和所有屬性直接添加到了Person的prototype屬性中,構(gòu)造函數(shù)變成了空函數(shù)。即使如此,也仍然可以通過調(diào)用構(gòu)造函數(shù)來創(chuàng)建新對(duì)象,而且新對(duì)象還會(huì)具有相同的屬性和方法。
但與構(gòu)造函數(shù)模式不同的是,新對(duì)象的這些屬性和方法時(shí)由所有實(shí)例共享的。換言之,person1和person2訪問的都是同一組屬性和方法
要理解原型模式的工作原理,必須先理解ECMAScript中原型對(duì)象的性質(zhì)
理解原型對(duì)象只要?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ù)的指針。例如上述例子,Person.prototype.constructor 指向Person。
我們可以繼續(xù)為原型對(duì)象添加其他屬性和方法
創(chuàng)建了自定義構(gòu)造函數(shù)后,其原型對(duì)象默認(rèn)只會(huì)得到constructor屬性,至于其他方法,則都是從Object繼承而來。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新的實(shí)例后,該實(shí)例的內(nèi)部將包含一個(gè)指針(內(nèi)部屬性),指向構(gòu)造函數(shù)的原型對(duì)象。ECMAScript 5 中管這個(gè)叫[[Prototype]]。
雖然腳本中沒有標(biāo)準(zhǔn)的方式訪問[[Prototype]],但Firefox, Safari, Chrome 在每個(gè)對(duì)象都支持一個(gè)屬性__proto__;這個(gè)屬性對(duì)腳本則是完全不可見的。
這個(gè)鏈接存在于實(shí)例與構(gòu)造函數(shù)的原型對(duì)象之間,而不是存在于實(shí)例與構(gòu)造函數(shù)之間
圖6-1(148頁)展示了Person構(gòu)造函數(shù)、Person的原型屬性以及Person現(xiàn)有的兩個(gè)實(shí)例之間的關(guān)系。在此,Person.prototype指向了原型對(duì)象,而Person.prototype.constructor又指回了Person。原型對(duì)象中除了包含constructor屬性之外,還包括后來添加的其他屬性。Person的每個(gè)實(shí)例——person1和person2都包含一個(gè)內(nèi)部屬性,該屬性僅僅指向了Person.prototype;換言之,person1和person2 與構(gòu)造函數(shù)沒有直接的關(guān)系。此外,要格外注意的是,雖然這兩個(gè)實(shí)例都不包含屬性和方法,但我們卻可以調(diào)用person1.sayName()。這是通過查找對(duì)象屬性的過程來實(shí)現(xiàn)的。
雖然在所有實(shí)現(xiàn)中都無法訪問到[[Prototype]],但可以通過isPrototypeOf()方法來確定對(duì)象之間是否存在這種關(guān)系。
console.log(Person.prototype.isPrototypeOf(person1)); // true console.log(Person.prototype.isPrototypeOf(person2)); // true console.log(Person.isPrototypeOf(person1)); // false console.log(Person.isPrototypeOf(person2)); // false
ECMAScript 5 新增了一個(gè)方法,Object.getPrototypeOf(), 在所有支持的實(shí)現(xiàn)中,這個(gè)方法返回[[Prototype]]的值。支持的瀏覽器 IE9+, Safari 5+, Opera 12+, Chrome
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true console.log(Object.getPrototypeOf(person1).name); // "Nicholas"
每當(dāng)代碼讀取某個(gè)對(duì)象的某個(gè)屬性時(shí),都會(huì)執(zhí)行一次搜索,目標(biāo)是具有給定名字的屬性。搜索從對(duì)象實(shí)例本身開始,如果沒有找到,則繼續(xù)搜索指針指向的的原型對(duì)象,在原型對(duì)象中查找具有給定名字的屬性。也就是說,在我們調(diào)用person1.sayName()的時(shí)候,會(huì)先執(zhí)行兩次搜索,在person1中沒有找到sayName屬性,繼續(xù)在person1的原型中搜索,在Person.prototype中找到了sayName屬性,然后讀取那個(gè)保存在原型對(duì)象中的函數(shù)。
原型最初只包含constructor屬性,而該屬性也是共享的,因此可以通過對(duì)象實(shí)例訪問
雖然可以通過對(duì)象實(shí)例訪問保存在原型中的值,但卻不能通過對(duì)象實(shí)例重寫原型中的值。如果我們?cè)趯?shí)例中添加了一個(gè)屬性,而該屬性與實(shí)例原型中的一個(gè)屬性同名,那我們就在實(shí)例中創(chuàng)建該屬性,該屬性將會(huì)屏蔽原型中的那個(gè)屬性
hasOwnProperty()方法(繼承于Object)可以檢測(cè)一個(gè)屬性是存在于實(shí)例中,還是存在于原型中。
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(); console.log(person1.hasOwnProperty("name")); // false person1.name = "Greg"; console.log(person1.name); // "Greg" ——來自實(shí)例 console.log(person1.hasOwnProperty("name")); // true console.log(person2.name); // "Nicholas" ——來自原型 console.log(person2.hasOwnProperty("name")); // false // 使用delete操作符刪除實(shí)例中的屬性 delete person1.name; console.log(person1.name); // "Nicholas" ——來自實(shí)例 console.log(person1.hasOwnProperty("name")); // false
ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法只能用于實(shí)例屬性,要取得原型屬性的描述符,必須直接在原型對(duì)象上調(diào)用Object.getOwnPropertyDescriptor() 方法
原型與in操作符兩種方式使用in操作符,多帶帶使用和在for-in循環(huán)中使用。
多帶帶使用in操作符,會(huì)在通過對(duì)象能夠訪問給定屬性時(shí)返回true,無論該屬性存在于實(shí)例中還是原型中。
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(); console.log(person1.hasOwnProperty("name")); // false console.log("name" in person1); // true person1.name = "Greg"; console.log("name" in person1); // true console.log(person2.name); // "Nicholas" ——來自原型 console.log("name" in person2); // true // 使用delete操作符刪除實(shí)例中的屬性 delete person1.name; console.log("name" in person1); // true
同時(shí)使用hasOwnProperty() 方法和in操作符,就可以確定該屬性存在于實(shí)例中還是存在運(yùn)行中
function hasPrototypeProperty(object, name) { return !object.hasOwnProperty(name) && (name in object); }
由于in操作符只要通過能夠訪問到屬性就返回true, hasOwnProperty() 只在屬性存在于實(shí)例中才返回true,因此只要in操作符返回true,而hasOwnproperty()返回false,就可以確定屬性是原型中的屬性。
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(); console.log(hasPrototypeProperty(person, "name")); // true person.name = "Greg"; console.log(hasPrototypeProperty(person, "name")); // false
使用for-in循環(huán),返回的是所有能夠通過對(duì)象訪問的、可枚舉的(enumerad)屬性,其中既包括存在于實(shí)例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉的屬性(即將[[Enumerable]]標(biāo)記為false的屬性)實(shí)例屬性也會(huì)在for-in循環(huán)返回,因?yàn)楦鶕?jù)規(guī)定,所有開發(fā)人員定的屬性,都是可枚舉的——只有在IE8及更早版本中例外。
IE早期版本的實(shí)現(xiàn)中存在一個(gè)bug,即屏蔽不可枚舉屬性的實(shí)例屬性不會(huì)出現(xiàn)在for-in循環(huán)中。
var o = { toString: function() { return "My Object"; } }; for (var prop in o) { if (prop == "toString") { console.log("Found toString"); // 在IE中不會(huì)顯示 } }
在IE中,由于其實(shí)現(xiàn)認(rèn)為原型的toString()方法被打上了值為false的[[Enumerable]]標(biāo)記,因此應(yīng)該跳過該屬性,結(jié)果就不會(huì)打印。該bug會(huì)影響默認(rèn)不可枚舉的所有屬性和方法,包括:hasOwnProperty(), propertyIsEnumerable(), toLocaleString(), valueOf()
ECMAScript 5 也將constructor和prototype屬性的[Enumerable]]特性設(shè)置為false,但并不是所有瀏覽器都照此實(shí)現(xiàn)。
要取得對(duì)象上所有可枚舉的實(shí)例屬性,可以使用ECMAScript5的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 p1kyes = Object.keys(p1); console.log(p1kyes); // "name,age"
如果你想要得到所有實(shí)例屬性,無論它是否可枚舉,都可以使用Object.getOwnPropertyNames()方法。
var keys = Object.getOwnPropertyNames(Person.prototype); // "constructor,name,age,job,sayName"
注意結(jié)果中包含了不可枚舉的constructor屬性。Object.keys() 和 Object.getOwnPropertyNames()方法都可以用來替代for-in循環(huán)。支持這兩個(gè)方法的瀏覽器有IE9+, Firefox4+, Safari5+, Opera12+, Chrome
更簡單的原型語法前面例子中每添加一個(gè)屬性和方法就要敲一遍 Person.prototype。 為減少不必要的輸入,也為了從視覺上更好的封裝原型的功能,更常見的做法是用一個(gè)包含所有屬性和方法的對(duì)象字面量來重寫整個(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了。因?yàn)槊縿?chuàng)建一個(gè)函數(shù),就會(huì)同時(shí)創(chuàng)建它的prototype對(duì)象,整個(gè)對(duì)象也會(huì)自動(dòng)獲得constructor屬性。而我們?cè)谶@里使用的語法,本質(zhì)上完全重寫了默認(rèn)的prototype對(duì)象,因此constructor屬性也就變成了新對(duì)象的constructor屬性(指向Object構(gòu)造函數(shù)),不再指向Person函數(shù)。此時(shí)盡管instanceof操作符還能返回正確的結(jié)果,但通過constructor已經(jīng)無法確定對(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); // Object
如果constructor的值真的很重要,可以像下面這樣特意將它設(shè)置回適當(dāng)?shù)闹怠?/p>
function Person() {} Person.prototype = { constructor: Person, // 讓 prototype的constructor重新指向Person name: "Nicholas", age: 29, job: "Software Engineer", sayName: function () { console.log(this.name); } };
這種方式重設(shè)constructor會(huì)導(dǎo)致它的[[Enumerable]]特性被設(shè)置為true。默認(rèn)情況下,原生的constructor屬性是不可枚舉的,因此如果你使用兼容ECMAScript 5 的 JavaScript 引擎,可以試一試 Object.defineProperty()
function Person() {} Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName: function () { console.log(this.name); } }; // 重設(shè)構(gòu)造函數(shù),只適用于ECMASCript 5 兼容的瀏覽器 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });原型的動(dòng)態(tài)性
由于在原型中查找值的過程是一次搜索,因此我們對(duì)原型對(duì)象所做的任何修改都能夠立即從實(shí)例上反映出來——即使是先創(chuàng)建了實(shí)例后修改原型也照樣如此
var friend = new Person(); Person.prototype.sayHi = function() { console.log("Hi"); }; friend.sayHi(); // "Hi"
盡管可以隨時(shí)為原型添加屬性和方法,并且修改能夠立即在所有對(duì)象實(shí)例中反映出來,但如果是重寫整個(gè)原型對(duì)象,那么情況就不一樣了。調(diào)用構(gòu)造函數(shù)時(shí)會(huì)為實(shí)例添加一個(gè)指向最初原型的[[Protoype]]指針,而吧原型修改為另外一個(gè)對(duì)象,就等于切斷了構(gòu)造函數(shù)與最初原型之間的聯(lián)系。請(qǐng)記?。簩?shí)例中的指針僅指向原型,而不是指向構(gòu)造函數(shù)。
function Person() {} var friend = new Person(); // 重寫整個(gè)原型對(duì)象,就等于切斷了構(gòu)造函數(shù)與最初原型之間的聯(lián)系 Person.prototype = { constructor: Person, age: 29, job: "Software Engineer", sayName: function () { console.log(this.name); } }; friend.sayName(); // error原生對(duì)象的原型
原型模式的重要性不僅體現(xiàn)在創(chuàng)建自定義類型方面,就連所有原生的引用類型,都是采用這種模式創(chuàng)建的。所有原生引用類型(Object, Array, String, 等)都在其構(gòu)造函數(shù)的原型上定義了方法。例如,在Array.prototype 中可以找到sort()方法,而在String.prototype中可以找到substring()方法修改同一個(gè)屬性
console.log(typeof Array.prototype.sort); // "function" console.log(typeof String.prototype.substring); // "function"
通過原生對(duì)象的原型,不僅可以取得所有默認(rèn)方法的引用,而且也可以定義新方法??梢韵裥薷淖远x對(duì)象的原型一樣修改原生對(duì)象的原型,因此可以隨時(shí)添加方法。下面的代碼就給基本包裝類型String添加了一個(gè)名為startsWith()的方法
String.prototype.startsWith = function (text) { return this.indexOf(text) == 0; }; var msg = "Hello world!"; console.log(msg.startsWith("Hello")); // true
盡管看起來很方便,但不推薦在產(chǎn)品化的程序修改原生對(duì)象的原型。如果某個(gè)實(shí)現(xiàn)中缺少某個(gè)方法,就在原生對(duì)象的原型中添加這個(gè)方法,那么當(dāng)另一個(gè)支持該方法的實(shí)現(xiàn)中運(yùn)行代碼時(shí),就可能會(huì)導(dǎo)致命名沖突。而且這樣做也可能會(huì)意外的重寫原生方法。
原型對(duì)象的問題
原型模式也不是沒有缺點(diǎn)。
首先,它省略了為構(gòu)造函數(shù)傳遞初始化參數(shù)這一環(huán)節(jié),結(jié)果所有實(shí)例在默認(rèn)情況下都將取得相同的屬性值。雖然這回在某種程度上帶來一些不方便,但還不是原型的最大問題
原型模式最大的問題是由其共享的本性鎖導(dǎo)致的。原型中所有屬性是被很多實(shí)例共享的,這種共享對(duì)于函數(shù)非常適合。對(duì)于那些包含基本值的屬性倒也說得過去,畢竟通過在實(shí)例上添加一個(gè)同名屬性,可以隱藏原型中的對(duì)應(yīng)屬性。
然而,對(duì)于包含引用類型值得屬性來說,問題就比較突出了
function Person() {} Person.prototype = { constructor: Person, // 讓 prototype的constructor重新指向Person name: "Nicholas", age: 29, job: "Software Engineer", friend: ["Shelby", "Court"], sayName: function () { console.log(this.name); } }; var person1 = new Person(); var person2 = new Person(); // 這里修改的實(shí)際上是Person.prototype.friends person1.friends.push("Van"); // 不但person1的friends屬性被修改,person2也做了同樣的改動(dòng) console.log(person1.friends); // "Shelby,Court,Van" console.log(person1.friends); // "Shelby,Court,Van" // 因?yàn)閮蓚€(gè)實(shí)例的friends屬性指向的都是Person.prototype.friends console.log(person1.friends === person2.friends); // true
上述問題正是很少有人多帶帶使用原型模式的原因的所在
組合使用構(gòu)造函數(shù)模式和原型模式創(chuàng)建自定義類型的最常見方式,就是組合使用構(gòu)造函數(shù)模式與原型模式。構(gòu)造函數(shù)模式用于定義實(shí)例屬性,而原型模式用于定義方法和共享的屬性。這樣每個(gè)實(shí)例都會(huì)有自己的一份實(shí)例屬性的副本,但又同時(shí)共享著對(duì)方法的引用,最大限度的節(jié)省了內(nèi)存。
另外這種混成模式還支持向構(gòu)造函數(shù)傳遞參數(shù)
這種構(gòu)造函數(shù)與原型混成的模式,是目前在ECMAScript 中使用最廣泛,認(rèn)同度最高的一種創(chuàng)建自定義類型的方法。可以說是用來定義引用類型的一種默認(rèn)模式。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } 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("Van"); console.log(person1.friends); // "Shelby,Court,Van" console.log(person1.friends); // "Shelby,Court" console.log(person1.friends === person2.friends); // false console.log(person1.sayName === person2.sayName); // true動(dòng)態(tài)原型模式
動(dòng)態(tài)原型模型把所有信息都封裝在了構(gòu)造函數(shù)中,而通過在構(gòu)造函數(shù)中初始化原型(僅在必要的情況下),又保持了同時(shí)使用構(gòu)造函數(shù)和原型的優(yōu)點(diǎn)。換言之,可以通過檢查某個(gè)應(yīng)該存在的方法是否有效,來決定是否需要初始化原型
function Person(name, age, job) { // 屬性 this.name = name; this.age = age; this.job = job; // 方法 if (typeof this.sayName != "function") { Person.perototype.sayName = function() { console.log(this.name); }; } } var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName(); // "Nicholas"
使用動(dòng)態(tài)原型模型時(shí),不能使用對(duì)象字面量重寫原型。如果在已經(jīng)創(chuàng)建了實(shí)例的情況下重寫原型,那么就會(huì)切斷現(xiàn)有實(shí)例與新原型之間的聯(lián)系。
寄生構(gòu)造函數(shù)模式在前述幾種模式都不適用的情況下,可以使用寄生(parasitic)構(gòu)造函數(shù)模式。創(chuàng)建一個(gè)函數(shù),該函數(shù)的作用僅僅是封裝創(chuàng)建的對(duì)象代碼,然后再返回新創(chuàng)建的對(duì)象;但從表面上看,這個(gè)函數(shù)又很像是典型的構(gòu)造函數(shù)
function Person(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 friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName(); // "Nicholas"
除了使用new操作符并把使用的包裝函數(shù)叫做構(gòu)造函數(shù)之外,這個(gè)模式跟工廠模式其實(shí)是一模一樣的。構(gòu)造函數(shù)再不返回值得情況下,默認(rèn)會(huì)返回新對(duì)象實(shí)例。而通過一個(gè)return語句,可以重寫調(diào)用構(gòu)造函數(shù)時(shí)返回的值。
這個(gè)模式可以在特殊情況下用來為對(duì)象創(chuàng)建構(gòu)造函數(shù)。假設(shè)我們想創(chuàng)建一個(gè)具有額外方法的特殊數(shù)組。由于不能直接修改Array構(gòu)造函數(shù),因此可以使用這個(gè)模式。
function SpecialArray() { // 創(chuàng)建數(shù)組 var values = new Array(); // 添加值 values.push.apply(values, arguments); // 添加方法 values.toPipedString = function() { return this.join("|"); }; // 返回?cái)?shù)組 return values; } var colors = new SpecialArray("red", "blue", "green"); console.log(colors.toPipedString()); // "red|blue|green"
關(guān)于寄生構(gòu)造函數(shù)模式,有一點(diǎn)需要說明:首先,返回的對(duì)象與構(gòu)造函數(shù)或者與構(gòu)造函數(shù)的原型屬性之間沒有關(guān)系;也就是說,構(gòu)造函數(shù)返回的對(duì)象與構(gòu)造函數(shù)外部創(chuàng)建的對(duì)象沒有什么不同。為此不能依賴instanceof操作符來確定對(duì)象類型。由于上述問題,我們建議在可以使用其他模式的情況下,不要使用寄生模式
穩(wěn)妥構(gòu)造函數(shù)模式
道格拉斯·克羅克福德(Douglas Crockford)發(fā)明了JavaScript中的穩(wěn)妥對(duì)象(durable objects)這個(gè)概念。穩(wěn)妥對(duì)象,指的是沒有公共屬性而且其方法不引用this的對(duì)象。穩(wěn)妥對(duì)象最適合在一些安全的環(huán)境中(這些環(huán)境中會(huì)禁止使用this和new),或者在防止數(shù)據(jù)被其他應(yīng)用程序(如Mashup程序)改動(dòng)時(shí)使用。穩(wěn)妥構(gòu)造函數(shù)遵循與寄生構(gòu)造函數(shù)類似的模式,但有兩點(diǎn)不同:
一是創(chuàng)建對(duì)象的實(shí)例方法不引用this
二是不適用new操作符調(diào)用構(gòu)造函數(shù)
function Person(name, age, job) { // 創(chuàng)建要返回的對(duì)象 var o = new Object(); // 這里定義私有變量和函數(shù) ... // 添加方法 o.sayName = function() { console.log(name); }; // 返回對(duì)象 return o; } var friend = Person("Nicholas", 29, "Software Engineer"); friend.sayName(); // "Nicholas"
在這種模式創(chuàng)建的對(duì)象中,除了使用sayName()方法之外,沒有其他辦法訪問name的值。
與寄生構(gòu)造函數(shù)模式類似,使用穩(wěn)妥構(gòu)造函數(shù)模式創(chuàng)建的對(duì)象與構(gòu)造函數(shù)之間也沒有什么關(guān)系,因此instanceof操作符對(duì)這種對(duì)象也沒有意義
繼承
許多OO語言都支持兩種繼承方式
接口繼承,只繼承方法簽名
實(shí)現(xiàn)繼承,繼承實(shí)際方法
如前所述,在ECMAScript中無法實(shí)現(xiàn)接口繼承,只支持實(shí)現(xiàn)繼承,而且其實(shí)現(xiàn)繼承主要是依靠原型鏈來實(shí)現(xiàn)的。
原型鏈
原型鏈 實(shí)現(xiàn)繼承的主要方法。基本思想是利用原型讓一個(gè)引用類型繼承另一個(gè)引用類型的屬性和方法。簡單回顧一下構(gòu)造函數(shù)、原型和實(shí)例的關(guān)系:
每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象
原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針
而實(shí)例都包含一個(gè)指向原型對(duì)象的內(nèi)部指針。
假如我們讓原型對(duì)象等于另一個(gè)類型的實(shí)例,顯然,此時(shí)的原型對(duì)象將包含一個(gè)指向另一個(gè)原型的指針,相應(yīng)地,另一個(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(); SubType.prototype.getSubValue = function () { return this.subproperty; }; var instance = new SubType(); console.log(instance.getSuperValue()); //true
上述代碼中,我們沒有使用SubType默認(rèn)使用的原型,而是給它換了個(gè)新原型,SuperType的實(shí)例。于是新原型不僅具有作為一個(gè)SuperType的實(shí)例所擁有的全部屬性和方法,而且其內(nèi)部還有一個(gè)指針,指向了SuperType的原型。
最終:
instance指向SubType的原型
SubType的原型又指向SuperType的原型
getSuperValue() 方法仍然還在SuperType.prototype中,但property則位于SubType.prototype中。這是因?yàn)閜roperty是一個(gè)實(shí)例屬性,而getSuperValue()則是一個(gè)原型方法。 既然 SubType.prototype 現(xiàn)在是SuperType的實(shí)例,那么property當(dāng)然就位于該實(shí)例中了。
此外,要注意instance.constructor 現(xiàn)在指向的是SuperType,這是因?yàn)樵瓉鞸ubType.prototype 中的 constructor被重寫了的緣故
通過實(shí)現(xiàn)原型鏈,本質(zhì)上拓展了原型搜索機(jī)制。當(dāng)讀取模式訪問一個(gè)實(shí)例屬性時(shí),首先會(huì)在實(shí)例中搜索該屬性。如果沒有找到該屬性,則會(huì)繼續(xù)搜索實(shí)例的原型。在通過原型鏈實(shí)現(xiàn)繼承的情況下,搜索過程就得以沿著原型鏈繼續(xù)向上。調(diào)用instance.getSuperValue()會(huì)經(jīng)歷三個(gè)搜索步驟
搜索實(shí)例
搜索SubType.prototype
搜索SuperType.prototype 最終找到方法
別忘記默認(rèn)的原型事實(shí)上,前述例子的原型鏈少了一環(huán)。所有引用類型默認(rèn)都繼承了Object,而這個(gè)繼承也是通過原型鏈實(shí)現(xiàn)的。所有函數(shù)的默認(rèn)原型都是Object的實(shí)例,因?yàn)槟J(rèn)原型都會(huì)包含一個(gè)內(nèi)部指針,指向Object.prototype。這也正是所有自定義類型都會(huì)繼承toString(), valueOf()等默認(rèn)方法的根本原因。
SubType繼承了SuperType,而 SuperType繼承了Object。當(dāng)調(diào)用instance.toString()方法,實(shí)際上調(diào)用的是保存在Object.prototype中的那個(gè)方法
確定原型和實(shí)例的關(guān)系第一種方式,使用instanceof操作符,只要用這個(gè)操作符來測(cè)試實(shí)例與原型鏈中出現(xiàn)過的構(gòu)造函數(shù),結(jié)果就會(huì)返回true。
// 由于原型鏈,我們可以是instance是Object,SuperType, SubType中任何一個(gè)類型的實(shí)例 console.log(instance instanceof Object); // true console.log(instance instanceof SuperType); // true console.log(instance instanceof SubType); // true
第二種方式,使用isPrototypeOf()方法。同樣,只要是原型鏈中出現(xiàn)過的原型,都可以說是該原型鏈所派生的實(shí)例的原型。
console.log(Object.prototype.isPrototypeOf(instance)); // true console.log(SuperType.prototype.isPrototypeOf(instance)); // true console.log(SubType.prototype.isPrototypeOf(instance)); // true謹(jǐn)慎的定義方法
子類型有時(shí)候需要覆蓋超類型中的一個(gè)方法,或者需要添加超類型中不存在的某個(gè)方法。但不管怎么樣,給原型鏈添加方法的代碼一定要放在替換原型的語句之后
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } // 繼承了SuperType,原來默認(rèn)的原型被替換 SubType.protoype = new SuperType(); // 添加新方法 SubType.prototype.getSubValue = function () { return this.subproperty; }; // 重寫超類型中的方法 SubType.protoype.getSuperValue = function () { return false; }; var instance = new SubType(); console.log(instance.getSuperValue()); // false
還有一點(diǎn)需要提醒讀者,即在通過原型鏈實(shí)現(xiàn)繼承時(shí),不能使用對(duì)象字面量創(chuàng)建原型方法。因?yàn)檫@樣就會(huì)重寫原型鏈
function SuperType() { this.property = true; } SuperType.prototype. getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } // 繼承了SuperType SubType.protype = new SuperType(); // 使用字面量添加新方法,會(huì)導(dǎo)致上一行代碼無效 // 原型鏈被切斷——SubType 和 SuperType 之間已經(jīng)沒有關(guān)系 SubType.prototype = { getSubValue: function () { return this.subproperty; }, someOtherMethod: function () { return false; } }; var instance = new SubType(); console.log(instance.getSuperValue())); // error原型鏈的問題
最主要的問題,來自包含引用類型值的原型。包含引用類型值的原型屬性,會(huì)被所有實(shí)例共享;所以要在構(gòu)造函數(shù)中定義值而不是原型對(duì)象。在通過原型來實(shí)現(xiàn)繼承時(shí),原型實(shí)際上會(huì)變成另一個(gè)類型的實(shí)例。于是,原先的 實(shí)例屬性也就順理成章的變成了 現(xiàn)在的原型屬性了。
function SuperType() { this.colors = ["red", "blue", "green"]; } function SubType() { } // SubType 繼承了 SuperType 之后 // SubType.prototype 就變成了 SuperType的一個(gè)實(shí)例 // 因此 SubType.prototype 也擁有了自己的colors屬性 等價(jià)于創(chuàng)建了一個(gè)SubType.prototype.colors SubType.protype = new SuperType(); var instance1 = new SubType(); instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black" // 結(jié)果就是所有SubType實(shí)例都會(huì)共享這個(gè)colors屬性 var instance2 = new SubType(); console.log(instance2.colors); // "red,blue,green,black"
原型鏈的第二個(gè)問題:在創(chuàng)建子類型的實(shí)力時(shí),不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)。準(zhǔn)確的說是沒有辦法在不影響所有對(duì)象實(shí)例的情況下,給超類型的構(gòu)造函數(shù)傳遞參數(shù)。
有鑒于此,實(shí)踐中很少會(huì)多帶帶使用原型鏈
借用構(gòu)造函數(shù)在解決原型中包含引用類型值所帶來的問題過程中,開發(fā)人員開始使用一種叫做 借用構(gòu)造函數(shù)(constructor stealing) 的技術(shù)(有時(shí)候也叫偽造對(duì)象或經(jīng)典繼承)。
思想相當(dāng)簡單,即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù)。函數(shù)只不過是在特定環(huán)境中執(zhí)行代碼的對(duì)象,因此通過使用apply() call()方法也可以在(將來)新創(chuàng)建對(duì)象上執(zhí)行構(gòu)造函數(shù)。實(shí)際上是在(未來將要)新創(chuàng)建的SubType實(shí)例環(huán)境下,調(diào)用了SuperType構(gòu)造函數(shù),就會(huì)在新 SubType 對(duì)象上執(zhí)行 SuperType() 函數(shù)中定義的所有對(duì)象初始化代碼。結(jié)果每個(gè)SubType的實(shí)例都會(huì)具有自己的colors屬性的副本了。
function SuperType() { this.colors = ["red", "blue", "green"]; } function SubType() { // 繼承了SuperType // "借調(diào)“了超類型的構(gòu)造函數(shù) SuperType.call(this); } var instance1 = new SubType(); instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black" // SubType實(shí)例都不會(huì)共享這個(gè)colors屬性 var instance2 = new SubType(); console.log(instance2.colors); // "red,blue,green"傳遞參數(shù)
相對(duì)于原型鏈而言,借用構(gòu)造函數(shù)有一個(gè)很大的優(yōu)勢(shì),即可以在子類型構(gòu)造函數(shù)中向超類型構(gòu)造函數(shù)傳遞參數(shù)。
為了確保SuperType 構(gòu)造函數(shù)不會(huì)重寫子類型的屬性,可以在調(diào)用超類型構(gòu)造函數(shù)后,再添加應(yīng)該在子類型中定義的屬性。
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.anme); // "Nicholas" console.log(instance.age); // 29借用構(gòu)造函數(shù)的問題
僅僅是借用構(gòu)造函數(shù),也將無法避免構(gòu)造函數(shù)模式存在的問題——方法都在構(gòu)造函數(shù)中定義,因此函數(shù)復(fù)用就無從談起了。
在超類型的原型中定義方法,對(duì)子類型而言也是不可見的,結(jié)果所有類型都只能使用構(gòu)造函數(shù)模式。
有鑒于此,借用構(gòu)造函數(shù)也是很少多帶帶使用的
組合繼承組合繼承(combination inheritance),有時(shí)候也叫作偽經(jīng)典繼承,是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一起,從而發(fā)揮二者之長。其背后的思路是使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承,而通過借用構(gòu)造函數(shù)來實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承。既通過在原型上定義方法實(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) { // 繼承屬性 SuperType.call(this, name); // 子類型自己的屬性 this.age = age } // 繼承方法 SubType.prototype = new SuperType(); // 如果不指定constructor,SubType.prototype.constructor 為 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); // "red,blue,green,black" instance1.sayName(); // "Nicholas" instance1.sayAge(); // 29 var instance2 = new SubType("Greg", 27); console.log(instance2.colors); // "red,blue,green" instance2.sayName(); // "Greg" instance2.sayAge(); // 27
組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了它們的優(yōu)點(diǎn),成為JavaScript中最常用的繼承模式。而且instanceof 和 isPrototypeOf() 也能夠用于識(shí)別基于組合繼承創(chuàng)建的對(duì)象。
原型式繼承道格拉斯·克羅克福德2006年在文章中介紹了一種實(shí)現(xiàn)繼承的方法,這種方法并沒有使用嚴(yán)格意義上的構(gòu)造函數(shù)。他的想法是借助原型可以基于已有對(duì)象創(chuàng)建新對(duì)象,同時(shí)還不必因此創(chuàng)建自定義類型。為達(dá)到這個(gè)目的,給出了如下函數(shù)
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í)類型的新實(shí)例。從本質(zhì)上講,object() 對(duì)傳入其中的對(duì)象執(zhí)行了一次淺復(fù)制
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"], }; var anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); console.log(anotherPerson.friends); // "Shelby,Court,Van,Rob" var yetAnotherPerson = object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); console.log(yetAnotherPerson.friends); // "Shelby,Court,Van,Rob,Barbie" console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
這種原型式繼承,要求你必須有一個(gè)對(duì)象可以作為另一個(gè)對(duì)象的基礎(chǔ)。把它傳遞給object()函數(shù),然后再根據(jù)具體需求對(duì)得到的對(duì)象加以修改即可。這意味著,person.friends不僅屬于person,而且也會(huì)被anotherPerson, yetAnotherPerson共享。實(shí)際上就相當(dāng)于又創(chuàng)建了person對(duì)象的兩個(gè)副本(淺拷貝)。
ECMAScript 5 通過新增Object.create() 方法規(guī)范化了原型式繼承。這個(gè)方法接受兩個(gè)參數(shù):一個(gè)用作新對(duì)象原型的對(duì)象和(可選)一個(gè)新對(duì)象定義額外的屬性的對(duì)象。在傳入一個(gè)參數(shù)的情況下,Object.create() 與 Object()方法的行為相同(原著如此表述,但實(shí)際兩者并不相同,參照第五章 Object類型 的相關(guān)補(bǔ)充說明)
var person = { name: "Nicholoas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); console.log(anotherPerson.friends); // "Shelby,Court,Van,Rob" var yetAnotherPerson = Object.object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); console.log(yetAnotherPerson.friends); // "Shelby,Court,Van,Rob,Barbie" console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
Object.create() 方法的第二個(gè)參數(shù)與Object.defineProperties() 方法的第二個(gè)參數(shù)格式相同:每個(gè)屬性都是通過自己的描述符定義的。以這種方式制定的任何屬性都會(huì)覆蓋對(duì)象上同名的屬性。
var person = { name: "Nicholoas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person, { name: { value: "Greg" } }); console.log(anotherPerson.name); // "Greg"
支持Object.create()方法的瀏覽器:IE9+, Firefox4+, Opera12+, Chrome
在沒有必要興師動(dòng)眾的創(chuàng)建構(gòu)造函數(shù),而只想讓一個(gè)對(duì)象與另一個(gè)對(duì)象保持類似的情況下,原型式繼承是完全可以勝任的。但別忘了,包含引用類型值(如上面的friends屬性是一個(gè)數(shù)組)始終都會(huì)共享相應(yīng)的值,就像使用原型模式一樣。
寄生式繼承寄生式繼承(parasitic)是與原型式繼承緊密相關(guān)的一種思路,并且同樣也是由克羅克福德推而廣之的。
思路與構(gòu)造函數(shù)和工廠模式類似,既創(chuàng)建了一個(gè)僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方式來增強(qiáng)對(duì)象,最后再像真的是它做了所有工作一樣返回對(duì)象。
function createAnother(original) { var clone = object(original); // 通過調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象 clone.sayHi = function() { // 以某種方式增強(qiáng)這個(gè)對(duì)象 console.log("hi"); }; return clone; // 返回這個(gè)對(duì)象 }寄生組合式繼承
組合繼承是最常用的繼承模式;不過最大的問題就是無論什么情況下,都會(huì)調(diào)用兩次超類型構(gòu)造函數(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; } // 實(shí)例化SuperType作為SubType的原型 // 立即觸發(fā) SubType.prototype = new SuperType(); // 第一次調(diào)用SuperType() SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { console.log(this.age); }; // 此時(shí)觸發(fā)第二次調(diào)用 var instance = new SubType("Nicholas", 29); console.log(instance.name); // "Nicholas" console.log(SubType.prototype.name); // undefined
在第一次調(diào)用SuperType構(gòu)造函數(shù)時(shí),SubType.prototype會(huì)得到兩個(gè)屬性:name和colors;它們都是SuperType的實(shí)例屬性,只不過現(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è)同名屬性。(圖6-6)
有兩組name和colors屬性,一組在實(shí)例instance上,一組在SubType的原型中。這就是調(diào)用兩次SuperType構(gòu)造函數(shù)的結(jié)果
所謂寄生組合式繼承,即通過借用構(gòu)造函數(shù)來繼承屬性,通過原型鏈的混成形式來繼承方法。其背后的思路是:不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù),我們所需要 的無非就是超類型原型的一個(gè)副本。本質(zhì)上,就是使用寄生式繼承來繼承超類型的原型,然后再將結(jié)果指定給子類型的原型
function inheritPrototype(subType, superType) { // 創(chuàng)建對(duì)象 - 超類型的對(duì)象原型的副本 // 這里沒有使用new操作符,所以沒有生成SuperType的實(shí)例 // 這里沒有調(diào)用SuperType構(gòu)造函數(shù) var prototype = Object(superType.prototype) console.log(prototype == superType.prototype) // 增強(qiáng)對(duì)象 - 彌補(bǔ)因重寫原型而失去的默認(rèn)constructor屬性 // 而這也將導(dǎo)致supert.prototype.constructor指向了subType prototype.constructor = subType; // 指定對(duì)象 - 將新創(chuàng)建的對(duì)象(即副本)賦值給子類型的原型。 subType.prototype = prototype; } 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); };
inheritPrototype() 函數(shù)實(shí)現(xiàn)了寄生組合式繼承的最簡單形式。這個(gè)函數(shù)接受兩個(gè)參數(shù): 子類型構(gòu)造函數(shù)和超類型構(gòu)造函數(shù)。
這樣就只調(diào)用了一次SuperType構(gòu)造函數(shù),并且因此便了SubType.prototype 上創(chuàng)建不必要的、多余的屬性。與此同時(shí),原型鏈還能保持不變;因此,還能夠正常使用instanceof 和 isPrototypeOf().
開發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類型最理想的繼承范式。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/109479.html
摘要:表示應(yīng)該立即下載腳本,但不應(yīng)妨礙頁面中的其他操作可選。表示通過屬性指定的代碼的字符集。表示腳本可以延遲到文檔完全被解析和顯示之后再執(zhí)行。實(shí)際上,服務(wù)器在傳送文件時(shí)使用的類型通常是,但在中設(shè)置這個(gè)值卻可能導(dǎo)致腳本被忽略。 第1章 JavaScript 簡介 雖然JavaScript和ECMAScript通常被人們用來表達(dá)相同的含義,但JavaScript的含義比ECMA-262要多得多...
摘要:具體說就是執(zhí)行流進(jìn)入下列任何一個(gè)語句時(shí),作用域鏈就會(huì)得到加長語句的塊。如果局部環(huán)境中存在著同名的標(biāo)識(shí)符,就不會(huì)使用位于父環(huán)境中的標(biāo)識(shí)符訪問局部變量要比訪問全局變量更快,因?yàn)椴挥孟蛏纤阉髯饔糜蜴湣? 基本類型和引用類型的值 ECMAscript變量包含 基本類型值和引用類型值 基本類型值值的是基本數(shù)據(jù)類型:Undefined, Null, Boolean, Number, String ...
摘要:定義函數(shù)表達(dá)式的方式有兩種函數(shù)聲明。不過,這并不是匿名函數(shù)唯一的用途。可以使用命名函數(shù)表達(dá)式來達(dá)成相同的結(jié)果閉包匿名函數(shù)和閉包是兩個(gè)概念,容易混淆。匿名函數(shù)的執(zhí)行環(huán)境具有全局性,因此其對(duì)象通常指向通過改變函數(shù)的執(zhí)行環(huán)境的情況除外。 定義函數(shù)表達(dá)式的方式有兩種: 函數(shù)聲明。它的重要特征就是 函數(shù)聲明提升(function declaration hoisting) 即在執(zhí)行代碼之前會(huì)...
摘要:引用類型的值對(duì)象是引用類型的一個(gè)實(shí)例。引用類型是一種數(shù)據(jù)結(jié)構(gòu),用于將數(shù)據(jù)和功能組織在一起。對(duì)數(shù)組中的每一項(xiàng)運(yùn)行給定函數(shù),如果該函數(shù)對(duì)任一項(xiàng)返回,則返回。組零始終代表整個(gè)表達(dá)式。所以,使用非捕獲組較使用捕獲組更節(jié)省內(nèi)存。 引用類型的值(對(duì)象)是引用類型的一個(gè)實(shí)例。 引用類型是一種數(shù)據(jù)結(jié)構(gòu),用于將數(shù)據(jù)和功能組織在一起。它同行被稱為類,但這種稱呼并不妥當(dāng),盡管ECMAScript從技術(shù)上講...
摘要:技術(shù)的核心是對(duì)象即。收到響應(yīng)后,響應(yīng)的數(shù)據(jù)會(huì)自動(dòng)填充對(duì)象的屬性,相關(guān)的屬性有作為響應(yīng)主體被返回的文本。收到響應(yīng)后,一般來說,會(huì)先判斷是否為,這是此次請(qǐng)求成功的標(biāo)志。中的版本會(huì)將設(shè)置為,而中原生的則會(huì)將規(guī)范化為。會(huì)在取得時(shí)報(bào)告的值為。 Ajax(Asynchronous Javascript + XML)技術(shù)的核心是XMLHttpRequest對(duì)象,即: XHR。雖然名字中包含XML,但...
摘要:用戶代理檢測(cè)用戶代理檢測(cè)是爭議最大的客戶端檢測(cè)技術(shù)。第二個(gè)要檢測(cè)是。由于實(shí)際的版本號(hào)可能會(huì)包含數(shù)字小數(shù)點(diǎn)和字母,所以捕獲組中使用了表示非空格的特殊字符。版本號(hào)不在后面,而是在后面。除了知道設(shè)備,最好還能知道的版本號(hào)。 檢測(cè)Web客戶端的手段很多,各有利弊,但不到萬不得已就不要使用客戶端檢測(cè)。只要能找到更通用的方法,就應(yīng)該優(yōu)先采用更通用的方法。一言蔽之,先設(shè)計(jì)最通用的方案,然后再使用特定...
閱讀 2843·2023-04-26 01:02
閱讀 1863·2021-11-17 09:38
閱讀 791·2021-09-22 15:54
閱讀 2899·2021-09-22 15:29
閱讀 888·2021-09-22 10:02
閱讀 3432·2019-08-30 15:54
閱讀 2007·2019-08-30 15:44
閱讀 1586·2019-08-26 13:46