學習怎樣創建對象是理解面向對象編程的第一步,第二步是理解繼承。在傳統的面向對象編程語言中,類繼承其他類的屬性。然而,JS的繼承方式與傳統的面向對象編程語言不同,繼承可以發生對象之間,這種繼承的機制是我們已經熟悉的一種機制:原型。 1.原型鏈接和Object.prototype js內置的繼承方式被稱 ...
學習怎樣創建對象是理解面向對象編程的第一步,第二步是理解繼承。在傳統的面向對象編程語言中,類繼承其他類的屬性。
然而,JS的繼承方式與傳統的面向對象編程語言不同,繼承可以發生對象之間,這種繼承的機制是我們已經熟悉的一種機制:原型。
1.原型鏈接和Object.prototype
js內置的繼承方式被稱為原型鏈接(prototype chaining)或原型繼承(prototypal inheritance)。正如我們在前一天所學的,原型對象上定義的屬性,在所有的對象實例中都是可用的,這就是繼承的一種形式。對象實例繼承了原型中的屬性。而原型也是一個對象,所以它也有自己的原型,並且繼承原型中的屬性。這被稱為原型鏈:對象繼承自己原型對象中屬性,而這個原型會繼續向上繼承自己的原型,依此類推。
所有對象,包括我們定義自己的對象,都自動繼承自Object
,除非我們另有指定(本課後面討論)。更具體地說,所有對象都繼承Object.prototype
。任何通過對象字面量定義的對象都有一個__proto__
設置為object.prototype
,意味著它們都繼承Object.prototype
對象中的屬性,就像這個例子中的book
:
var book = {
title: "平凡的世界"
};
var prototype = Object.getPrototypeOf(book);
console.log(prototype === Object.prototype); // true
book
的原型等於Object.prototype
。不需要額外的代碼來實現這一點,因為這是創建新對象時的預設行為。這種關係意味著book
自動接收來自Object.prototype
對象中的方法。
1.2.從Object.prototype中繼承的方法
我們在前幾天使用的一些方式實際上是定義在Object.prototype
原型對象中,因此所有其他對象也都繼承了這些方法。這些方法是:
- hasOwnProperty():判斷對象中有沒有某個屬性,接受一個字元串類型的屬性名作為參數。
- propertyIsEnumerable():判斷對象中的某個屬性是否是可枚舉的。
- isPrototypeOf():判斷一個對象是否是另個對象的原型。
- valueOf:返回對象的值表示形式。
- toString:返回對象的字元串表示形式。
- toLocaleString: 返回對象的本地字元串表示形式。
這五種方法通過繼承所有對象都擁有這6個方法。當我們需要使對象在JavaScript中一致工作時,最後兩個是非常重要的,有時我們可能希望自己定義它們。
1.3:valueOf()
當我們操作對象時,valueof()
方法就會被調用時。預設情況下,valueof()
簡單地返回對象實例。對於字元串,布爾值和數字類型的值,首先會使用原始包裝類型包裝成對象,然後再調用valueof()
方法。同樣,Date
對象的valueof()
方法返回以毫秒為單位的紀元時間(就像Date.prototype.getTime()
一樣)。這也是為什麼我們可以對日期進行比較,例如:
var now = new Date();
var earlier = new Date(2010, 1, 1);
console.log(now > earlier); // true
1.4修改Object.prototype
預設情況下,所有對象都繼承自Object.prototype
,因此改變Object.prototype
會影響到所有對象。這是非常危險的情況。
Object.prototype.add = function(value) {
return this + value;
};
var book = {
title: "平凡的世界"
};
console.log(book.add(5)); // "[object Object]5"
console.log("title".add("end")); // "titleend"
// in a web browser
console.log(document.add(true)); // "[object HTMLDocument]true"
console.log(window.add(5)); // "[object Window]true"
導致的另一個問題:
var empty = {};
for (var property in empty) {
console.log(property);
}
解決方法:
for(name in book){
if(book.hasOwnProperty(name)){
console.log(name);
}
}
雖然這個方法可以有效地過濾掉我們不需要的原型屬性,但是它也限制了使用for-in
只能變數的屬性,而不能遍歷原型屬性。建議不要修改原型對象。
2:對象繼承
最簡單的繼承方式是對象之間的繼承。我們所需要做的就是指定新創建對象的原型應該指向哪個對象。通過Object字面量的形式創建的對象預設將__proto__
屬性指向了Object.prototype
,但是我們可以通過Object.create()
方法顯示地將__proto__
屬性指向其他對象。
Object.create()
方法接收兩個參數。第一個參數用來指定新創建對象的__proto__
應該指向的對象。第二個參數是可選的,用來設置對象屬性的描述符(特性),語法格式與Object.definedProperties()
方法參數個格式一樣。如下所示:
var book = {
title: "人生"
};
// 等價於
var book = Object.create(Object.prototype, {
title: {
configurable: true,
enumerable: true,
value: "人生",
writable: true
}
});
代碼中兩個聲明的效果是一樣的。第一個聲明使用對象字面量的方式定義一個帶有單個屬性:title
的對象。這個對象自動繼承自Object.prototype
,並且屬性預設被設置成可配置,可枚舉,可寫。第二個聲明和第一個一樣,但是顯示使用了Object.create()
方法。但是你可能永遠不會這樣顯示地直接繼承Object.prototype
,沒有必要這樣做,因為預設就已經繼承了Object.prototype
。繼承自其他對象會比較有趣一點:
var person1 = {
name: '張三',
sayName: function(){
console.log(this.name);
}
};
var person2 = Object.create(person1, {
name: {
value: '李四',
configurable: true,
enumerable: true,
writable: true
}
});
person1.sayName(); // '張三'
person2.sayName(); // '李四'
console.log(person1.hasOwnProperty("sayName")); // true
console.log(person1.isPrototypeOf(person2)); // true
console.log(person2.hasOwnProperty("sayName")); // false
這段代碼創建了一個對象person1
,該對象有一個name
屬性和一個sayName()
方法。person2
對象繼承了person1
,因此它也繼承了name
屬性和sayName()
方法。然而,person2
是通過Object.create()
方法定義的,它也定義了自己的name
屬性。對象自己的屬性遮擋了原型的中同名屬性name
。因此,person1.sayName()
輸出'張三'
,person2.sayName()
輸出'李四'
。記住,person2.sayName()
只存在於person1
中,被person2
繼承了下來。
當對象的屬性被訪問時,JavaScript會首先會在對象的屬性中搜索,如果沒有找到,則繼續在__proto__
指向的原型對象中搜索。如果任然沒有找到,則繼續搜索原型對象的上個原型對象,直到到達原型鏈的末端。原型鏈的末端結束於Object.prototype
,Object.prototype
對象的__proto__
內部屬性為null
。
3.構造函數繼承
JavaScript中的對象繼承也是構造函數繼承的基礎。回顧昨天的內容,幾乎每一個函數都有一個可以修改或替換的prototype
屬性。prototype
屬性自動被賦值為一個新的對象,這個對象繼承自Object.prototype
,並且對象中有一個自己的屬性constructor
。實際上,JavaScript引擎為我們執行以下操作:
// 這是我們寫的
function YourConstructor() {
// initialization
}
// JavaScript引擎在後臺幫我們做的:
YourConstructor.prototype = Object.create(Object.prototype, {
constructor: {
configurable: true,
enumerable: true,
value: YourConstructor
writable: true
}
});
因此,不做任何額外的工作,這段代碼給我們的構造函數的prototype
屬性設置了一個對象,這個對象繼承自Object.prototype
,這意味著通過構造函數YourConstructor()
創建的所有實例都繼承自Object.prototype
。YourConstructor
是Object
的子類,Object
是YourConstructor
的超類。
由於prototype
屬性是可寫的,因此通過覆寫它我們可以改變原型鏈。例如:
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
Rectangle.prototype.toString = function() {
return "[Rectangle " + this.length + "x" + this.width + "]";
};
// 繼承 Rectangle
function Square(size) {
this.length = size;
this.width = size;
}
Square.prototype = new Rectangle();
Square.prototype.constructor = Square;
Square.prototype.toString = function() {
return "[Square " + this.length + "x" + this.width + "]";
};
var rect = new Rectangle(5, 10);
var square = new Square(6);
console.log(rect.getArea()); // 50
console.log(square.getArea()); // 36
console.log(rect.toString()); // "[Rectangle 5x10]"
console.log(square.toString()); // "[Square 6x6]"
console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Object); // true
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square instanceof Object); // true
這段代碼中有兩個構造函數:Reactangle
和Square
。Square
構造函數的原型對象被重新賦值為Reactangle
的對象實例。在創建Reactangle
對象實例的時候沒有傳遞參數,因為它們沒有用,如果傳遞參數了,所有的Square
對象實例都會共用相同的尺寸。以這種方式改變原型鏈之後,要確保constructor
屬性的指向正確的構造函數。