摘要:構造函數本身也是函數,只不過可以用來創建對象而已。在創建子類型的實例時,沒有辦法在不影響所有對象實例的情況下,不能向超類型的構造函數中傳遞參數。借用構造函數又叫偽造對象或經典繼承。
本章內容
理解對象屬性
理解并創建對象
理解繼承
ECMA-262 把對象定義為:“無序屬性的集合,其屬性可以包含基本值、對象或者函數。”嚴格來講,這就相當于說對象是一組沒有特定順序的值。
每個對象都是基于一個引用類型創建的,既可以是原生類型,也可以是開發人員定義的類型。
6.1 理解對象創建對象最簡單的方式就是創建一個 Object 的實例,然后為它添加屬性和方法。
var person = { name: "Jack", age: 29, sayName: function() { alert(this.name); } }
這些屬性在創建時都帶有一些特征值(characteristic),JS 通過這些特征值來定義它們的行為。
6.1.1 屬性類型ECMAScript 中有兩種屬性:數據屬性和訪問器屬性。
1. 數據屬性數據屬性包含一個數據值的位置。在這個位置可以讀取和寫入值。數據屬性有4個描述其行為的特性。
[[Configurable]]
[[Enumerable]]
[[Writable]]
[[Value]]
對于直接在對象上定義的屬性,它們的 [[Configurable]]、[[Enumerable]]、[[Writable]] 特性都被設置為 true,而 [[Value]] 特性被設置為指定的值。
要修改屬性默認的特性,必須使用 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。
2. 訪問器屬性訪問器屬性不包含數據值;它們包含一對 getter/setter 函數(不過,這兩個函數都不是必須的)。在讀取訪問器屬性時,會調用 getter 函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用 setter 函數并傳入新值,這個函數負責決定如何處理數據。特性如下:
[[Configurable]]
[[[Enumerable]]
[[Get]]:在讀取屬性時調用的函數,默認值 undefined。
[[Set]]:在寫入屬性時調用的函數,默認值 undefined。
訪問器屬性不能直接定義,必須使用 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個參數:1、第一個對象是要添加和修改其屬性的對象;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++; } } })6.1.3 讀取屬性的特性
使用 ECMAScript 5 中的 Object.getOwnPropertyDescriptor() IE9+ 方法,可以取得給定屬性的描述符。這個方法接收兩個參數:1、屬性所在的對象;2、要讀取器描述符的屬性名稱。返回值是一個對象,如果是數據屬性,這個對象的屬性有 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"6.2 創建對象
問題:使用同一個接口創建很多對象,會產生大量的重復代碼。
6.2.1 工廠模式工廠模式抽象了創建具體對象的過程。用函數來封裝以特定接口創建對象的細節。
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);
工廠模式雖然解決了創建多個相似對象的問題,但是沒有解決對象識別的問題(即怎樣知道一個對象的類型)。
6.2.2 構造函數模式可以創建自定義的構造函數,從而定義自定義對象類型的屬性和方法。
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);
構造函數模式有以下幾個特點:
沒有顯示地創建對象;
直接將屬性和方法賦給了this對象;
沒有return語句。
函數名開頭必須大寫。
構造函數本身也是函數,只不過可以用來創建對象而已。
要創建 Person 的新實例,必須使用 new 操作符。以這種方式調用構造函數實際上會經歷以下4個過程:
創建一個新對象;
將構造函數的作用域賦給新對象(因此 this 就指向了這個新對象);
執行構造函數中的代碼(為這個新對象添加屬性);
返回新對象。
使用 instanceof 檢測對象類型:
alert(person1 instanceof Object); // true alert(person1 instanceof Person); // true alert(person2 instanceof Object); // true alert(person2 instanceof Person); // true
創建自定義的構造函數意味著將來可以將它的實例標識為一種特定的類型;而這正是構造函數模式勝過工廠模式的地方。
1. 將構造函數當做函數// 當做構造函數來使用 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"2. 構造函數的問題
每個方法都要在每個實例上重新創建一遍。在前面的例子中,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);
但新問題是:在全局作用域定義的函數實際上只能被某個對象調用,這讓全局作用域名不副實。而且,如果對象需要定義很多方法,那么就要定義多個全局函數,于是這個自定義的引用類型就沒有絲毫封裝性可言。
6.2.3 原型模式每個函數都有一個 prototype(原型)屬性,這個屬性是一個指針,指向一個對象,這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。
也可以說 prototype 就是通過調用構造函數而創建的對象實例的原型對象。使用原型對象的好處是可以讓所有“對象實例”共享“原型對象”所包含的屬性和方法。
6. 原型對象的問題它省略了為構造函數傳遞初始化參數這一環節,結果所有實例在默認情況下都將取得相同的屬性值。
原型模式的最大問題是它的共享的本性所導致的。這個問題在包含引用類型值的屬性上顯而易見。
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 都指向同一個數組,導致修改其中一個,就會在另一個上同步共享。
6.2.4 組合使用構造函數模式和原型模式構造函數模式用于定義實例屬性,原型模式用于定義方法和共享的屬性。
每個實例都會擁有自己的一份實例屬性的副本,但同時又共享著對“方法”的引用,最大限度地節約了內存。
這種混成模式還支持向構造函數傳遞參數。
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
混成模式中,不同實例引用了不同的數組,因此原型對象的問題解決了。
6.2.5 動態原型模式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 操作符確定它的類型。
使用動態原型模式時,不能使用對象字面量重寫原型,如果重寫,則會切斷現有實例與新原型之間的聯系。
6.2.6 寄生構造函數模式 6.2.7 穩妥構造函數模式 6.3 繼承由于函數沒有簽名,在ECMAScript 中無法實現【接口繼承】。ECMAScript 只支持【實現繼承】,而且其實現繼承主要依靠【原型鏈】來實現。
6.3.1 原型鏈基本思想
利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
構造函數、原型、實例之間的關系:
每個構造函數都有一個原型對象;
原型對象都有一個指向構造函數的指針;
實例都包含一個指向原型對象的內部指針[[Prototype]]
實現原型鏈的基本模式:
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 中。
1. 默認原型所有應用類型默認都繼承了 Object,而這個繼承也是通過原型鏈實現的。所有函數的默認原型都是 Object 的實例,因此默認原型都會包含一個內部指針,指向 Object.prototype。這也是所有自定義類型都會繼承 toString()valueOf() 等默認方法的根本原因。
2. 確定原型和實例的關系可以通過兩種方式來確定原型和實例之間的關系。
方法一: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)); // true3. 謹慎地定義方法
子類型有時候需要覆蓋超類型中的某個方法,或者需要添加超類型中不存在的某個方法。給原型添加方法的代碼一定要放在替換原型的語句之后。
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!4. 原型鏈的問題
最主要的問題來自包含引用類型值的原型。包含引用類型值的原型屬性會被所有實例共享;而這也正是為什么要在構造函數中,而不是在原型對象中定義屬性的原因。在通過原型來實現繼承時,原型實際上會變成另一個類型的實例。于是,原先的實例屬性也就順理成章地變成了現在的原型屬性了。
在創建子類型的實例時,沒有辦法在不影響所有對象實例的情況下,不能向超類型的構造函數中傳遞參數。因此,實踐中很少會多帶帶使用原型鏈。
6.3.2 借用構造函數(constructor stealing)又叫“偽造對象”或“經典繼承”。
基本思想
在子類型構造函數的內部調用超類型構造函數。函數只不過是在特定環境中執行代碼的對象,因此通過使用 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"1. 傳遞參數
相對于原型鏈而言,借用構造函數有一個很大的有時,可以在子類型構造函數中向超類型構造函數傳遞參數。
function A(name) { this.name = name; } function B() { // 繼承了 A A.call(this, "Jack"); } var instance1 = new B(); alert(instance1.name); // "Jack"
為了確保 A 構造函數不會重寫子類型的屬性,可以在調用超類型構造函數后,再添加應該在子類型中定義的屬性。
2. 借用構造函數的問題方法都在構造函數中定義,因此函數復用就無從談起;
在超類型的原型中定義的方法,對子類型而言也是不可見的,結果所有類型都只能使用構造函數模式。因此借用構造函數也很少多帶帶使用。
6.3.3 組合繼承(combination inheritance)又叫“偽經典繼承”,組合了原型鏈繼承和借用構造函數繼承。既通過在原型上定義方法實現了函數服用,又能保證每個實例都擁有自己的屬性。
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() 也能夠用于識別基于組合繼承創建的對象。
組合模式的問題
無論什么情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,一次是在子類型構造函數內部。
6.3.4 原型式繼承function object(o) { function F(){}; F.prototype = o; return new F(); }
原型式繼承要求必須有一個對象作為另一個對象的基礎。
ECMAScript 5 中新增了 Object.create() 來規范原型式繼承。接收2個參數:1、一個用做新對象原型的對象;2、(可選)一個為新對象定義額外屬性的對象。在傳入一個參數的情況下,Object.create() 和 object() 的行為相同。
var person = {}; var anotherPerson = Object.create(person);
如果只想讓一個對象與另一個對象保持類似的情況下,原型式繼承完全可以勝任。但是包含引用類型值的屬性始終都會共享相應的值,這點與原型模式一樣。
6.3.5 寄生式繼承(parasitic)它的思路與寄生構造函數和工廠模式相似,即創建一個僅用于封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后再像真地是它做了所有工作一樣返回對象。
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() 并不是必需的;任何能夠返回新對象的函數都適用于該模式。
注意:使用寄生式繼承來為對象添加函數,會由于不能做到函數復用而降低效率;這一點與構造函數模式類似。
6.3.6 寄生組合式繼承本質上,就是使用“寄生式繼承”來繼承超類型的原型,再將結果指定給子類型的原型。
function inheritPrototype(sub, super) { var prototype = Object(super); // 創建對象 prototype.constructor = sub; // 增強對象 sub.prototype = prototype; // 指定對象 }
創建超類型原型的一個副本;
為創建的副本添加 constructor 屬性,從而彌補因【重寫原型】而失去的默認的 constructor 屬性;
將新創建的對象(即副本)賦值給子類型的原型。
修改之前的例子:
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() 方法。開發人員普遍認為寄生組合式繼承是引用類型最理想的繼承范式。
6.4 小結ECMAScript 支持面向對象(OO)變成,但不使用類或者接口。對象可以在代碼執行過程中創建和增強,因此具有動態性而非嚴格定義的實體。在沒有類的情況下,可以采用下列模式創建對象:
工廠模式,使用簡單的函數創建對象,為對象添加屬性和方法,然后返回對象。這個模式后來被構造函數所取代。
構造函數模式,可以創建自定義引用類型,可以像創建內置對象實例一樣使用 new 操作符。不過,構造函數模式的缺點是:它的每個成員都無法得到復用,包括函數。由于函數可以不局限于任何對象(即與對象具有松散耦合的特點),因此沒有理由不在多個對象間共享函數。
原型模式,使用構造函數的 prototype 屬性來指定那些應該共享的屬性和方法。組合使用構造函數模式和原型模式時,使用構造函數定義實例屬性,使用原型模式定義共享的屬性和方法。
JS 主要通過原型鏈實現繼承。原型鏈的構建是通過將一個類型的實例賦值給另外一個構造函數的原型實現的。這樣,子類型就可以繼承超類型的屬性和方法,這一點與基于類的繼承很相似。
原型鏈的問題是:對象實例共享所有繼承的屬性和方法,因此不適宜多帶帶使用。解決這個問題的技術是借用構造函數,即在子類型構造函數的內部調用超類型構造函數。這樣就可以做到每個實例都具有自己的屬性,同時還能保證只使用構造函數模式來定義類型。
使用最多的繼承模式是組合繼承,這種模式使用原型鏈繼承共享的屬性和方法,通過借用構造函數繼承實例屬性。
此外,還存在下列可供選擇的繼承模式:
原型式繼承,可以在不必預先定義構造函數的情況下實現繼承,其本質是執行對給定對象的淺復制。而復制得到的副本還可以得到進一步改造。
寄生式繼承,與原型式繼承非常相似,也是基于某個對象或某些信息創建一個對象,然后增強該對象,最后返回對象。為了解決組合繼承模式由于多次調用超類型構造函數而導致的低效率問題,可以將這個模式與組合繼承模式一起使用。
寄生組合式繼承,集寄生式繼承與組合繼承的優點于一身,是實現基于類型繼承的最有效方式。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106761.html
摘要:其中,描述符對象的屬性必須是和。吧設置為,表示不能從對象中刪除屬性。這個方法接收兩個對象參數要添加和修改其屬性值的對象,第二個是與第一個對象中要添加和修改的屬性值一一對應。 理解對象 1、創建自定義對象的兩種方法: (1)創建一個Object實例,然后再為它添加屬性和方法。 var person = new Object(); person.name = Nicholas; ...
摘要:然而事實上并不是。函數本身也是一個對象,但是給這個對象添加屬性并不能影響。一圖勝千言作者給出的解決方案,沒有麻煩的,沒有虛偽的,沒有混淆視線的,原型鏈連接不再赤裸裸。所以是這樣的一個函數以為構造函數,為原型。 注意:本文章是個人《You Don’t Know JS》的讀書筆記。在看backbone源碼的時候看到這么一小段,看上去很小,其實忽略了也沒有太大理解的問題。但是不知道為什么,我...
閱讀 2731·2023-04-25 22:15
閱讀 1810·2021-11-19 09:40
閱讀 2155·2021-09-30 09:48
閱讀 3223·2021-09-03 10:36
閱讀 2030·2021-08-30 09:48
閱讀 1858·2021-08-24 10:00
閱讀 2732·2019-08-30 15:54
閱讀 704·2019-08-30 15:54