Chapter 6 面向?qū)ο蟮某绦蛟O計
理解對象
-
使用對象字面量語法創(chuàng)建對象
var person = { name: "Nicholas", age: 29, job: "Software Engineer", sayName: function() { alert(this.name); } };
-
屬性類型
- 為了表示某些特性是內(nèi)部值周伦,ES5把這些attribute放到了兩對中括號中,例如[Enumerable]。
- ES中有兩種屬性:數(shù)據(jù)屬性和訪問器屬性
-
數(shù)據(jù)屬性:包含一個數(shù)據(jù)值的位置缰泡,在這個位置可以讀取和寫入值。數(shù)據(jù)屬性有4個描述其行為的特性:
- 修改數(shù)據(jù)屬性默認的特征,必須使用ES5的Object.defineProperty()方法打掘,接收三個參數(shù):屬性所在的對象、屬性名和一個描述符對象鹏秋。其中描述符對象的屬性必須是configurable enumerable writable value尊蚁。設置其中的一或多個值,可以修改對應的特征值侣夷。如果不指定默認為false横朋。
var person = {}; Object.defineProperty(person, "name", { writable: false, value: "Nicholas" } );
var person = {}; Object.defineProperty(person, "name", { configurable: false, value: "Nicholas" } ); Object.defineProperty(person, "name", { configurable: true, // 拋出錯誤:當configurable屬性被定義為false時,就再也不能變回true了 value: "Nicholas" } );
-
訪問器屬性:不包含數(shù)據(jù)值百拓,包含一對getter和setter函數(shù)(非必須)琴锭。在讀取訪問器屬性時,會調(diào)用getter函數(shù)耐版,該函數(shù)負責返回有效的值祠够;在寫入訪問器屬性時,調(diào)用setter函數(shù)并傳入新值粪牲,該函數(shù)負責決定如何處理數(shù)據(jù)古瓤。訪問器有如下4個特性:
- 訪問器必須使用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
- 只指定getter意味著屬性不能寫,嘗試寫入會被忽略落君;只指定setter意味著不能讀穿香,否則在非嚴格模式下會返回undefined。
-
-
定義多個屬性
- Object.defineProperties()
- 接收兩個對象參數(shù):第一個要添加和修改其屬性的對象绎速、第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應皮获。
var book = {}; Object.defineProperties(book, { _year: { writable: true, value: 2004 }, edition: { writable: true, value: 1 }, year: { get: function() { return this._year; }, set: function() { if(newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } } } );
-
讀取屬性的特征
- getOwnPropertyDescriptor() 取得給定屬性的描述符
- 接收兩個參數(shù):屬性所在的對象和要讀取其描述符的屬性名稱。返回一個對象:如果是訪問器屬性纹冤,這個對象的屬性有configurable enumerable get set洒宝,如果是數(shù)據(jù)屬性,這個對象的屬性有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() { if(newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } } } ); var descriptor = Object.getOwnPropertyDescription(book, "_year"); alert(descriptor.value); // 2004 alert(descriptor.configurable); // false alert(typeof descriptor.get); // undefined
創(chuàng)建對象
-
工廠模式
- 抽象創(chuàng)建具體對象的過程雁歌,生成函數(shù),用以封裝特定接口創(chuàng)建對象的細節(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 person = createPerson('Lawrence', 20, "Doctor");
-
構造函數(shù)模式
- 可以創(chuàng)建自定義構造函數(shù)靠瞎,從而定義自定義對象類型的屬性和方法。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { alert(this.name); }; } var person = new Person('Lawrence', 20, 'Doctor');
- 使用 new 操作符創(chuàng)建新實例求妹。過程:
- 創(chuàng)建一個新對象
- 將構造函數(shù)的作用域給新對象(因此this指向了這個新對象)
- 執(zhí)行構造函數(shù)中的代碼
- 返回新對象
- 對象的 constructor 屬性可以標識對象類型乏盐,例如 alert(person.constructor == Person); // true ,但是不建議使用制恍。
- 構造函數(shù)也是函數(shù)父能,直接調(diào)用構造函數(shù)會將函數(shù)定義的方法和屬性添加值其執(zhí)行環(huán)境對象上。
- 問題:每個方法都要在每個實例上重新創(chuàng)造一遍吧趣,所以會導致不同的作用域鏈和標識符解析法竞。所以不同實例上的同名函數(shù)是不相等的。解決方法:將函數(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); } // 會破壞封裝性
-
原型模式
- 創(chuàng)建的每個函數(shù)都有一個prototype屬性,這個屬性是一個指針薛躬,指向一個對象俯渤,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。即不必在構造函數(shù)中定義對象實例的信息型宝,而是可以將這些信息直接添加到原型對象中八匠。
function Person() { } Person.prototype.name = "Lawrence"; Person.prototype.age = 20; Person.prototype.job = "Doctor"; Person.prototype.sayName = function() { alert(this.name); } var person = new Person(); alert(person.sayName()); // "Lawrence"
-
原型對象:
- 創(chuàng)建了自定義構造函數(shù)之后,其原型對象默認只會取得constructor屬性趴酣。當調(diào)用構造函數(shù)創(chuàng)建一個新實例后梨树,該實例的內(nèi)部將包含一個指針(內(nèi)部屬性),指向構造函數(shù)的原型對象岖寞。
- 可以使用 isPrototypeOf() 方法來確定對象之間是否存在原型指向關系: alert(Person.prototype.isPrototypeOf(person)); // true
- 可以使用 getPrototypeOf() 方法來返回對象的原型:alert(Object.prototype.getPrototypeOf(person).name); // 'Lawrence'
- 每當代碼讀取某個對象的某個屬性時抡四,都會執(zhí)行一次搜索,目標是具有給定名字的屬性。搜索首先從對象實例本身開始指巡。如果在實例中找到了具有給定名字的屬性淑履,則返回該屬性的值(即使是null);如果沒有找到藻雪,則繼續(xù)搜索指針指向的原型對象秘噪,在原型對象中查找具有給定名字的屬性。如果在原型對象中找到了這個屬性,則返回該屬性的值荣赶。
- constructor屬性也是共享的靡菇,可以通過對象實例訪問。
- 不能通過對象實例重寫原型中的值至壤。
- 使用delete操作符可以完全刪除實例屬性,從而重新訪問到之前被實例屬性屏蔽掉的原型屬性椭住。
function Person() { } Person.prototype.name = "Lawrence"; var person = new Person(); person.name = "Qwerty"; alert(person.name); delete person.name; alert(person.name) ;
- 可以使用hasOwnProperty()方法檢測屬性是否存在于實例(不是原型)中:alert(person.hasOwnProperty('name')); // true
- 直接在原型上使用Object.getOwnPropertyDescriptor()以獲取原型屬性的描述符崇渗。
-
原型與 in 操作符
- in操作符返回true但是hasOwnProperty()返回false則說明屬性是原型中的屬性。所以可以定義如下函數(shù):
function hasPrototypeProperty(object, name) { return !object.hasOwnProperty(name) && (name in object); }
- 對對象使用for-in循環(huán)京郑,會返回所有能夠通過對象訪問的宅广、可枚舉([Enumerable]==true)的屬性。
- 可以使用Object.keys()方法獲取對象上所有可枚舉的實例屬性些举。
function Person() { } Person.prototype.name = 'Hello'; Person.prototype.age = 20; Person.prototype.sayName = function() { alert(this.name); }; var keys = Object.keys(Person.prototype); // "name,age,job,sayName" var person = new Person(); person.name = "Lawrence"; var pKeys = Object,keys(person); // "name"
-
更簡單的原型語法
- 使用對象字面量重寫prototype
function Person() { } Person.prototype = { name: 'Lawrence', age: 20, sayName: function() { alert(this.name); } }; // 會丟失 Person.prototype.constructor 屬性跟狱,轉(zhuǎn)而指向 Object.constructor 屬性 alert(person instanceof Person) // true alert(person.constructor == Person) // false alert(person.constructor == Object) // true // 應當在重寫prototype時手動添加constructor: Person.prototype = { constructor: Person, // 這樣設置會造成constructor可枚舉 name: 'Lawrence', age: 20, sayName: function() { alert(this.name); } }; // 使 constructor 不可枚舉 Object.defineProperty(Person.prototype, 'constructor', { enumerable: false, value: Person });
原型的動態(tài)性
可以隨時為原型添加屬性和方法,并且修改能夠立即在所有對象實例中反映出來户魏,但如果重寫整個原型對象驶臊,將會破壞[[Prototype]]指針。實例中的指針僅僅指向原型叼丑,而不指向構造函數(shù)关翎。
-
原型對象的問題
- 原型中所有的屬性是被很多實例共享的,這種共享對于函數(shù)非常合適鸠信。對于那些包含基本值的屬性纵寝,通過在實例上添加一個同名屬性可以隱藏原型中的對應屬性。然而對于包含引用類型值的屬性星立,則這些引用會共享爽茴,造成一些問題。
-
組合使用構造函數(shù)模式和原型模式(創(chuàng)建自定義類型的最常見方式)
- Example:
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); } }
-
動態(tài)原型模式
- 把所有信息都封裝在構造函數(shù)中绰垂,通過在構造函數(shù)中初始化原型(僅在必要的情況下)保持同時使用構造函數(shù)和原型的優(yōu)點室奏。換言之,可以通過檢查某個應該存在的方法是否有效劲装,來決定是否需要初始化原型胧沫。
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('Lawrence', 20, 'Doctor');
- 不能使用對象字面量重寫原型昌简!
-
寄生構造函數(shù)模式
- 創(chuàng)建一個函數(shù),其作用僅僅是封裝創(chuàng)建對象的代碼琳袄,然后再返回新創(chuàng)建的對象江场。
function Person (name, age, job) { var o = new Object(); o.name = name; o.age = age; o.sayName = function() { alert(this.name); }; return o; } var friend = new Person('Lawrence', 20, 'Doctor');
- 修改類似Array這樣不能直接修改其構造函數(shù)的類型的時候,可以使用此方法窖逗。
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());
- 構造函數(shù)返回的對象與構造函數(shù)的原型屬性之間沒有關系址否,不能依賴 instanceof 操作符確定對象的類型。
-
穩(wěn)妥構造函數(shù)模式
- 穩(wěn)妥對象指的是沒有公共屬性碎紊,而且其方法不引用this的對象佑附。最適合在一些安全的環(huán)境中、或者在防止數(shù)據(jù)被其他應用程序改動時使用仗考。
- 穩(wěn)妥構造函數(shù)類似寄生構造函數(shù)音同,但不使用this、不使用new秃嗜。
function Person(name) { var o = new Object(); this.name = name; o.sayName = function() { alert(name); } return o; } var friend = Person('Lawrence'); friend.sayName();
- 除了調(diào)用sayName()方法外权均,沒有別的方法能夠訪問到其數(shù)據(jù)成員。
- 構造函數(shù)返回的對象與構造函數(shù)的原型屬性之間沒有關系锅锨,不能依賴 instanceof 操作符確定對象的類型叽赊。
繼承
-
接口繼承 & 實現(xiàn)繼承
- 接口繼承:只繼承方法簽名
- 實現(xiàn)繼承:繼承實際的方法
-
原型鏈(實現(xiàn)繼承的主要方法)
- 利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
- 實現(xiàn)思路:讓原型對象等于另一個類型的實例必搞。
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; } function SubType() { this.subProperty = false; } SubType.prototype = new SuperType(); subType.prototype.getSubValue = function() { return this.subProperty(); } // *** 此時 SubType對象的constructor指向的是SuperType必指,因為SubType.prototype中的constructor被重寫了(SubType.prototype被指向了SuperType)。
- 實現(xiàn)基礎:搜索機制(見“原型對象”)恕洲。
- 確定原型和實例的關系
var instance = new SubType(); alert(instance instanceof SuperType); // true alert(instance instanceof SubType); // true alert(SuperType.prototype.isPrototypeOf(instance)); // true alert(SubType.prototype.isPrototypeOf(instance)); // true
給原型添加方法代碼一定要放在替換原型的語句(繼承語句)之后塔橡。
在使用原型鏈實現(xiàn)繼承時,不能使用對象字面量創(chuàng)建原型方法霜第。這樣做會重寫原型鏈葛家。
-
原型鏈的問題
- 在通過原型來實現(xiàn)繼承時,原型實際上會變成另一個類型的實例泌类。于是惦银,原先的實例屬性也就順理成章的變成了現(xiàn)在的原型的屬性了。
function SuperType() { this.colors = ['red']; } function SubType() { } SubType.prototype = new SuperType(); var instance_1 = new SubType(); instance_1.colors.push('black'); var instance_2 = new SubType(); alert(instance_2.colors); // 'red,black'
- 在創(chuàng)建子類型的實例時末誓,不能向超類型的構造函數(shù)中傳遞參數(shù)。
-
借用構造函數(shù)
- 在子類型構造函數(shù)的內(nèi)部調(diào)用超類型的構造函數(shù)书蚪。通過使用apply()和call()方法在新創(chuàng)建的對象上執(zhí)行構造函數(shù)喇澡。
function SuperType() { this.colors = ['red']; } function SubType() { SuperType.call(this); } var instance = new SubType(); instance_1.colors.push('black'); var instance_2 = new SubType(); alert(instance_2.colors); // 'red'
- 可以在子類型構造函數(shù)中向超類型傳遞參數(shù)。
function SuperType(name) { this.name = name; } function SubType() { SuperType.call(this, 'Lawrence'); }
- 借用構造函數(shù)的問題:方法都要在構造函數(shù)中定義殊校,無法實現(xiàn)函數(shù)復用晴玖。
-
組合繼承
- 將原型鏈和借用構造函數(shù)結合。使用原型鏈實現(xiàn)對原型屬性和方法的繼承,通過借用構造函數(shù)來對實例屬性進行繼承呕屎。
- 既能通過在原型上定義方法實現(xiàn)了函數(shù)復用让簿,又能保證每個實例都有它自己的屬性。
function SuperType(name) { this.name = name; this.colors = ['red']; } 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); }
-
原型式繼承
- 借助原型可以基于已有的對象創(chuàng)建新對象秀睛,同時不必因此創(chuàng)建自定義類型尔当。
function object(o) { function F(){} F.prototype = o; return new F();
- ES5新增了Object.create()方法規(guī)范化了原型式繼承。這個方法接收兩個參數(shù):一個用作新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象蹂安。
var person = { name: 'Lawrence', friends: ['Q', 'W', 'E'] }; var anotherPerson = Object.create(person); anotherPerson.name = 'Greg'; // 會覆蓋掉原型對象上的屬性 anotherPerson.friends.push('Qwerty');
-
寄生式繼承
- 與寄生構造函數(shù)和工廠模式類似椭迎,創(chuàng)建一個僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方式來增強對象田盈,最后再像真的是他做了一切工作一樣返回對象。
function createAnother(original) { var clone = object(original); clone.sayHi = function() { alert('Hi'); }; return clone; }
- 無法函數(shù)復用允瞧。
-
寄生組合式繼承
組合繼承問題:無論什么情況下,都會調(diào)用兩次超類型構造函數(shù):一次是在創(chuàng)建子類型原型的時候述暂,一次是在子類型構造函數(shù)內(nèi)部。
基本模式
function inheritPrototype(subType, superType) { var prototype = Object(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; }
- Example:
function SuperType(name) { this.name = name; this.colors = ['red']; } 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); };