本章內容數組
ECMA-262 把對象定義爲:「無序屬性的集合,其屬性能夠包含基本值、對象或者函數。」嚴格來說,這就至關於說對象是一組沒有特定順序的值。app
每一個對象都是基於一個引用類型建立的,既能夠是原生類型,也能夠是開發人員定義的類型。函數
建立對象最簡單的方式就是建立一個 Object 的實例,而後爲它添加屬性和方法。測試
var person = { name: "Jack", age: 29, sayName: function() { alert(this.name); } }
這些屬性在建立時都帶有一些特徵值(characteristic),JS 經過這些特徵值來定義它們的行爲。this
ECMAScript 中有兩種屬性:數據屬性和訪問器屬性。prototype
數據屬性包含一個數據值的位置。在這個位置能夠讀取和寫入值。數據屬性有4個描述其行爲的特性。指針
對於直接在對象上定義的屬性,它們的 [[Configurable]]、[[Enumerable]]、[[Writable]] 特性都被設置爲 true,而 [[Value]] 特性被設置爲指定的值。code
要修改屬性默認的特性,必須使用 ECMAScript 5 的 Object.defineProperty() 方法。接收3個參數:屬性所在的對象、屬性名字、一個描述符對象。其中描述符(descriptor)對象的屬性必須是:configurable/enumerable/writable/value。設置其中的一個或多個值,能夠修改對應的特性值。對象
var person = {} Object.defineProperty(person, "name", { writable: false, configurable: false, value: "Nick" }) alert(person.name); // Nick person.name = Jack; alert(person.name); // Nick delete person.name; alert(person.name); // Nick
注意:一旦把屬性定義爲不可配置的,就不能再把它變回可配置了。也就是說,能夠屢次調用 Object.defineProperty()方法修改同一個屬性,但在把 configurable 設置爲 false 後,就不能了。繼承
在調用 Object.defineProperty() 時,若是不指定,則 configurable/writable/enumerable 都爲 false。
訪問器屬性不包含數據值;它們包含一對 getter/setter 函數(不過,這兩個函數都不是必須的)。在讀取訪問器屬性時,會調用 getter 函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用 setter 函數並傳入新值,這個函數負責決定如何處理數據。特性以下:
訪問器屬性不能直接定義,必須使用 Object.defineProperty() 。
var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, "year", {// IE9+ 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。只指定 getter 表示屬性是不能寫,反之則表示屬性不能讀。
Object.defineProperties() 能夠經過描述符一次性定義多個屬性。接收2個參數:一、第一個對象是要添加和修改其屬性的對象;二、第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應。
var book = {}; Object.defineProperties(book, { // IE9+ _year: { writable: true, value: 2004 }, edition: { writable: true, value: 1 }, year: { get: function() { return this._year; }, set: function(newValue) { this._year = newValue; this.edition++; } } })
使用 ECMAScript 5 中的 Object.getOwnPropertyDescriptor() IE9+ 方法,能夠取得給定屬性的描述符。這個方法接收兩個參數:一、屬性所在的對象;二、要讀取器描述符的屬性名稱。返回值是一個對象,若是是數據屬性,這個對象的屬性有 configurable/enumerable/writable/value,若是是訪問器屬性,則這個對象的屬性有 configurable/enumerable/get/set
// 使用前面的例子 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"
問題:使用同一個接口建立不少對象,會產生大量的重複代碼。
工廠模式抽象了建立具體對象的過程。用函數來封裝以特定接口建立對象的細節。
function createPerson(name, age) { var o = new Object(); o.name = name; o.age = age; o.sayName = function() { alert(this.name); }; return o; } var person1 = createPerson("Jack", 29); var person2 = createPerson("Nick", 22);
工廠模式雖然解決了建立多個類似對象的問題,可是沒有解決對象識別的問題(即怎樣知道一個對象的類型)。
能夠建立自定義的構造函數,從而定義自定義對象類型的屬性和方法。
function Person(name, age) { this.name = name; this.age = age; this.sayName = function() { alert(this.name); } } var person1 = new Person("Jack", 23); var person2 = new Person("Nick", 22);
構造函數模式有如下幾個特色:
要建立 Person 的新實例,必須使用 new 操做符。以這種方式調用構造函數實際上會經歷如下4個過程:
使用 instanceof 檢測對象類型:
alert(person1 instanceof Object); // true alert(person1 instanceof Person); // true alert(person2 instanceof Object); // true alert(person2 instanceof Person); // true
建立自定義的構造函數意味着未來能夠將它的實例標識爲一種特定的類型;而這正是構造函數模式賽過工廠模式的地方。
// 當作構造函數來使用 var person = new Person("Nick", 29); person.sayName(); // "Nick" // 當作普通函數調用 Person("Nick", 29); // 添加到 window 對象 window.sayName(); // "Nick" // 在另外一個對象做用域中調用 var o = new Object(); Person.call(o, "Nick", 29); o.sayName(); // "Nick"
每一個方法都要在每一個實例上從新建立一遍。在前面的例子中,person1 和 person2 的 sayName() 方法並非同一個 Function 的實例。由於函數是對象,因此每定義一個函數,也就實例化了一個對象。(new Function())。
解決的辦法,能夠把函數定義移到構造函數外部。
function Person(name, age) { this.name = name; this.age = age; this.sayName = sayName; } function sayName() { alert(this.name); } var person1 = new Person("Jack", 23); var person2 = new Person("Nick", 22);
但新問題是:在全局做用域定義的函數實際上只能被某個對象調用,這讓全局做用域名存實亡。並且,若是對象須要定義不少方法,那麼就要定義多個全局函數,因而這個自定義的引用類型就沒有絲毫封裝性可言。
每一個函數都有一個 prototype(原型)屬性,這個屬性是一個指針,指向一個對象,這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。
也能夠說 prototype 就是經過調用構造函數而建立的對象實例的原型對象。使用原型對象的好處是可讓全部「對象實例」共享「原型對象」所包含的屬性和方法。
function Person() {} Person.prototype = { constructor: Person, friends: ["Jack"] }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Nick"); alert(person1.friends); // "Jack, Nick" alert(person2.friends); // "Jack, Nick" alert(person1.friends === person2.friends); // true
實例通常都是要有本身的所有屬性的,然而因爲 person1.friends 和 person2.friends 都指向同一個數組,致使修改其中一個,就會在另外一個上同步共享。
構造函數模式用於定義實例屬性,原型模式用於定義方法和共享的屬性。
function Person(name, age) { this.name = name; this.age = age; this.friends = ["Jack"]; } Person.prototype = { constructor: Person, sayName: function() { alert(this.name); } } var person1 = new Person("Nick", 22); var person2 = new Person("Mike", 21); person1.friends.push("Jane"); alert(person1.friends); // "Jack, Jane" alert(person2.friends); // "Jack" alert(person1.friends === person2.friends); // false alert(person1.sayName === person2.sayName); // true
混成模式中,不一樣實例引用了不一樣的數組,所以原型對象的問題解決了。
function Person(name, age) { // 屬性 this.name = name; this.age = age; // 方法 if (typeof this.sayName != "function") { Person.prototype.sayName: function() { alert(this.name); } } } var person1 = new Person("Nick", 22); person1.sayName();
if 語句檢查的能夠是初始化以後應該存在的任何屬性或方法——沒必要用一大堆 if 語句檢查每一個屬性和每一個方法,只要檢查其中一個便可。
對於採用這種模式建立的對象,可使用 instanceof 操做符肯定它的類型。
使用動態原型模式時,不能使用對象字面量重寫原型,若是重寫,則會切斷現有實例與新原型之間的聯繫。
因爲函數沒有簽名,在ECMAScript 中沒法實現【接口繼承】。ECMAScript 只支持【實現繼承】,並且其實現繼承主要依靠【原型鏈】來實現。
基本思想
利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。
構造函數、原型、實例之間的關係:
實現原型鏈的基本模式:
function A() { this.aproperty = true; } A.prototype.getAValue = function() { return this.property; }; function B() { this.bproperty = false; } // 繼承了 A,建立了 B 的實例,並將實例賦給 B.prototype B.prototype = new A(); B.prototype.getBValue = function() { return this.bproperty; } var instance = new B(); alert(instance.getAValue); // true
實現的本質是重寫原型對象,代之以一個新實例的類型。原來存在於 A 的實例中的全部屬性和方法,如今也存在於 B.prototype 中。
全部應用類型默認都繼承了 Object,而這個繼承也是經過原型鏈實現的。全部函數的默認原型都是 Object 的實例,所以默認原型都會包含一個內部指針,指向 Object.prototype。這也是全部自定義類型都會繼承 toString()valueOf() 等默認方法的根本緣由。
能夠經過兩種方式來肯定原型和實例之間的關係。
方法一:instanceof,只要用這個操做符來測試實例和原型鏈中出現過的構造函數,結果就會返回 true。
alert(instance instanceof Object); // true alert(instance instanceof A); // true alert(isntance instanceof B); // true
因爲原型鏈的關係,instance 是 Object、A、B 中任何一個類型的實例。
方法二:isPropertyOf,只要是原型鏈中出現過的原型,均可以說是該原型鏈所派生的實例的原型,所以該方法也會返回 true。
alert(Object.prototype.isPropertyOf(instance)); // true
子類型有時候須要覆蓋超類型中的某個方法,或者須要添加超類型中不存在的某個方法。給原型添加方法的代碼必定要放在替換原型的語句以後。
function A() { this.property = true; } A.prototype.getAValue = function() { return this.property; }; function B() { this.bproperty = false; } // 繼承了 A B.prototype = new A(); // 添加新方法 B.prototype.getBValue = function() { return this.bproperty; } // 重寫超類型方法 B.prototype.getAValue = function() { return false; }
注意,經過 A 的實例調用 getAValue() 方法時,仍然繼續調用原來的方法。
在經過原型鏈實現繼承時,不能使用對象字面量建立原型方法。由於這樣作會重寫原型鏈。
function A() { this.property = true; } A.prototype.getAValue = function() { return this.property; }; function B() { this.bproperty = false; } // 繼承了 A B.prototype = new A(); // 添加新方法 B.prototype = { getBValue: function() { return this.bproperty; } }; var instance = new B(); alert(instance.getAValue); // error!
又叫「僞造對象」或「經典繼承」。
基本思想
在子類型構造函數的內部調用超類型構造函數。函數只不過是在特定環境中執行代碼的對象,所以經過使用 apply() 和 call() 也能夠在(未來)新建立的對象上執行構造函數。
function A() { this.colors = ["red"]; } function B() { // 繼承了 A A.call(this); } var instance1 = new B(); instance1.colors.push("blue"); alert(instance1.colors); // "red, blue" var instance2 = new B(); alert(instance2.colors); // "red"
相對於原型鏈而言,借用構造函數有一個很大的有時,能夠在子類型構造函數中向超類型構造函數傳遞參數。
function A(name) { this.name = name; } function B() { // 繼承了 A A.call(this, "Jack"); } var instance1 = new B(); alert(instance1.name); // "Jack"
爲了確保 A 構造函數不會重寫子類型的屬性,能夠在調用超類型構造函數後,再添加應該在子類型中定義的屬性。
又叫「僞經典繼承」,組合了原型鏈繼承和借用構造函數繼承。既經過在原型上定義方法實現了函數服用,又能保證每一個實例都擁有本身的屬性。
function A(name) { this.name = name; this.colors = ["red"]; } A.prototype.sayName = function() { alert(this.name); }; function B(name, age) { // 繼承屬性 A.call(this, name); // 第二次調用 A this.age = age; } // 繼承方法 B.prototype = new A(); // 第一次調用 A B.prototype.constructor = B; B.prototype.sayAge = function() { alert(this.age); } var instance1 = new B("Jack", 22); instance1.colors.push("blue"); alert(instance1.colors); // "red, blue" instance1.sayName(); // "Jack" instance1.sayAge(); // 22 var instance2 = new B("Nick", 21); alert(instance2.colors); // "red" instance2.sayName(); // "Nick" instance2.sayAge(); // 21
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢,成爲JS中最經常使用的繼承模式。並且,instanceof 和 isPropertyOf() 也可以用於識別基於組合繼承建立的對象。
組合模式的問題
不管什麼狀況下,都會調用兩次超類型構造函數:一次是在建立子類型原型的時候,一次是在子類型構造函數內部。
function object(o) { function F(){}; F.prototype = o; return new F(); }
原型式繼承要求必須有一個對象做爲另外一個對象的基礎。
ECMAScript 5 中新增了 Object.create() 來規範原型式繼承。接收2個參數:一、一個用作新對象原型的對象;二、(可選)一個爲新對象定義額外屬性的對象。在傳入一個參數的狀況下,Object.create() 和 object() 的行爲相同。
var person = {}; var anotherPerson = Object.create(person);
若是隻想讓一個對象與另外一個對象保持相似的狀況下,原型式繼承徹底能夠勝任。可是包含引用類型值的屬性始終都會共享相應的值,這點與原型模式同樣。
它的思路與寄生構造函數和工廠模式類似,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真地是它作了全部工做同樣返回對象。
function createAnother(original) { var clone = object(original); // 經過調用函數建立一個新對象 clone.sayHi = function() { // 以某種方式加強對象 alert("Hi"); }; return clone; // 返回對象 } var person = { name: "Jack", friends: ["Nick", "Tony"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); // "Hi"
新對象不只具備 person 的全部屬性和方法,還有本身的方法。
在主要考慮「對象」而不是「自定義類型」和「構造函數」的狀況下,寄生式繼承也是一種有用的模式。object() 並非必需的;任何可以返回新對象的函數都適用於該模式。
注意:使用寄生式繼承來爲對象添加函數,會因爲不能作到函數複用而下降效率;這一點與構造函數模式相似。
本質上,就是使用「寄生式繼承」來繼承超類型的原型,再將結果指定給子類型的原型。
function inheritPrototype(sub, super) { var prototype = Object(super); // 建立對象 prototype.constructor = sub; // 加強對象 sub.prototype = prototype; // 指定對象 }
修改以前的例子:
function A(name) { this.name = name; } A.prototype.sayName = function() { alert(this.name); }; function B(age) { A.call(this, "Jack"); this.age = age; } inheritPrototype(B,A); B.prototype.sayAge = function() { alert(this.age); }
該模式的高效率體如今它只調用了一次 A 構造函數,而且所以避免了在 B 的 prototype 上面建立沒必要要的、多餘的屬性。與此同時,原型鏈還能保持不變;所以,還可以正常使用 instanceof 和 isPrototypeOf() 方法。開發人員廣泛認爲寄生組合式繼承是引用類型最理想的繼承範式。
ECMAScript 支持面向對象(OO)變成,但不使用類或者接口。對象能夠在代碼執行過程當中建立和加強,所以具備動態性而非嚴格定義的實體。在沒有類的狀況下,能夠採用下列模式建立對象:
JS 主要經過原型鏈實現繼承。原型鏈的構建是經過將一個類型的實例賦值給另一個構造函數的原型實現的。這樣,子類型就能夠繼承超類型的屬性和方法,這一點與基於類的繼承很類似。
原型鏈的問題是:對象實例共享全部繼承的屬性和方法,所以不適宜單獨使用。解決這個問題的技術是借用構造函數,即在子類型構造函數的內部調用超類型構造函數。這樣就能夠作到每一個實例都具備本身的屬性,同時還能保證只使用構造函數模式來定義類型。
使用最多的繼承模式是組合繼承,這種模式使用原型鏈繼承共享的屬性和方法,經過借用構造函數繼承實例屬性。
此外,還存在下列可供選擇的繼承模式: