前言 理解原型和原型鏈,有助於更好的理解JavaScript中的繼承機制。 最近比較有空,所以想寫一篇關於原型和原型鏈的文章,如寫得不好請見諒。 原型對象 無論什麼時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個 prototype 屬性,這個屬性指向函數的原型對象。在預設情況下, ...
前言
理解原型和原型鏈,有助於更好的理解JavaScript中的繼承機制。
最近比較有空,所以想寫一篇關於原型和原型鏈的文章,如寫得不好請見諒。
原型對象
無論什麼時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個prototype屬性,這個屬性指向函數的原型對象。在預設情況下,所有的原型對象都會自動獲得一個constructor屬性,這個屬性是一個指向prototype屬性所在函數的指針。
function Person(name) {
this.name = name
}
Person.prototype.age = 18
Person.prototype.sayName = function() {
alert(this.name)
}
var person = new Person('張三')
person.sayName() // 張三
上述我們創建了一個Person構造函數,並創建了一個name實例和原型對象的age屬性和sayName方法,接下來new實例對象都擁有這些屬性方法
Person構造函數的prototype屬性指向原型對象,person實例的[[prototype]]指向原型對象(在chrome瀏覽器中是__proto__),我們可以通過isPrototypeOf()
方法來確認對象之間是否存在原型關係。ES5新增了一個新方法,Object.PrototypeOf()
,這個方法返回[[prototype]]的值
alert(Peron.prototype.isPrototypeOf(person)) // true
alert(Object.PrototypeOf(person) == Person.prototype) // true
alert(Object.PrototypeOf(peroson).name // '張三'
當我們訪問一個實例對象的某個屬性時,如果在實例中找到具有給定名字的屬性,則返回屬性的值;如果沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性,如果在原型對象中找到具有給定名字的屬性,則返回該屬性的值
看下麵這段代碼
function Person() {
}
Person.prototype.name = 'zhangsan';
Person.prototype.age = 18;
Person.prototype.sayName = function() { alert(this.name) };
var person1 = new Person();
var person2 = new Person();
person1.name = 'lisi';
console.log(person1.name); // 'lisi'——來自實例
console.log(person2.name); // 'zhangsan'——來自原型
console.log(person1.hasOwnProperty('name')) // true
delete person1.name; // 'zhangsan'——來自原型
console.log(person1.name);
console.log(person1.hasOwnProperty('name')) // false
console.log(person2.hasOwnProperty('name')) // false
在這個例子中,person1的name被一個新值給屏蔽了。當為對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的同名屬性。即使將這個屬性設置為null,也只會在實例中設置這個屬性,而不會恢復其指向原型的連接。不過可以使用delete操作符刪除實例屬性,從而讓我們重新訪問原型中的屬性。
使用hasOwnProperty()
方法,可以檢測屬性是否來自實例,只在給定屬性存在於對象實例中時,才會返回true
我們還可以通過定義函數封裝一個檢測該屬性到底存在於對象中,還是存在於原型中的方法
function hasProtoypeProperty(object, name) {
return !object.hasProperty(name) && (name in object)
}
function Person() {
}
Person.prototype.name = 'zhangsan';
Person.prototype.age = 18;
Person.prototype.sayName = function() { alert(this.name) };
var person = new Person();
console.log(hasProtoypeProperty(person, 'name')); // true
person.name = 'lisi'
console.log(hasProtoypeProperty(person, 'name')); // false
很多同學可能註意到前面例子每添加一個屬性和方法就要敲一遍Person.prototype。為減少不必要的輸入,我們可以使用一個包含所有屬性和方法的對象字面量來重寫整個原型對象
function Person() {}
Person.prototype = {
name: 'zhangsan',
age: 19,
sayName: function() {
console.log(this.name)
}
}
上述代碼中有一個問題,Person.prototype中的constructor
屬性不再指向Person,此時我們需要手動將constructor
屬性指向Person
function Person() {}
Person.prototype = {
constructor: Person,
name: 'zhangsan',
age: 19,
sayName: function() {
console.log(this.name)
}
}
var person = new Person();
for(var val in person) {
console.log(val); // 'constructor','name','age','sayName'
}
以這種方式重設constructor
屬性會導致它的[[Enumerable]]特性被設置為true,導致可以通過for-in遍歷出來,最好的辦法是通過ES5中的Object.defineProperty()
方法設置constructor
屬性
function Person() {}
Person.prototype = {
name: 'zhangsan',
age: 19,
sayName: function() {
console.log(this.name)
}
}
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
原型還有一個特點,是具有動態性,即我們對原型對象所做的修改都能夠立即從實例上反映出來,即使是先創建實例後修改原型對象也是如此
function Person() { }
var person1 = new Person();
Person.prototype = {
name: 'zhangsan',
age: 19,
sayName: function () {
console.log(this.name)
}
}
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
person1.sayName(); // Uncaught TypeError: person.sayName is not a function
var person2 = new Person();
person2.sayName(); // zhangsan
前面提過,調用構造函數時會為實例添加一個指向最初原型的[[prototype]]指針,上述代碼中,重寫原型對象切斷了現有原型與任何之前已經存在的對象實例之間的聯繫。實例中的指針指向原型,而不指向構造函數。
原型鏈
在JavaScript中,原型鏈是實現繼承的主要的方法,理解原型鏈有助於我們理解JavaScript中的繼承機制。
原型鏈,其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法,我們都知道,每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針(constructor),實例對象都包含一個指向原型對象的內部指針(__proto__),讓原型對象等於另一個類型的實例,此時原型對象將包含一個指向另一個原型的指針,假如另一個原型又是另外一個類型的實例,如此層層遞進,就構成了實例與原型的鏈條,簡稱原型鏈。
在JavaScript中,每個實例對象都有一個__proto__屬性指向它的構造函數的原型對象prototype,原型對象也有一個自己的原型對象__proto__,層層向上直到一個對象的原型對象為null
,幾乎所有JavaScript中的對象都是位於原型鏈頂端的Object
的實例。
function Father() {
this.name = 'zhangsan';
}
Father.prototype.sayName = function() {
console.log(this.name);
}
function Son() {
this.age = 19
}
Son.prototype = new Father();
var xiaoming = new Son();
xiaoming.sayName(); // 'zhangsan'
上述代碼中,讓Son構造函數的原型指向Father構造函數的實例,此時就實現了JavaScript中最簡單的繼承原理。讓我們通過下圖來更好的理解原型鏈。
上圖很清晰的描繪了原型鏈中的繼承機制,對象最頂層Object的原型的原型是指向一個null
。註意,在為原型添加方法的代碼一定要放在替換原型的語句之後,也不能通過對象字面量創建原型方法,因為這樣做就會重寫原型鏈。
function Father() {
this.name = 'zhangsan';
}
Father.prototype.sayName = function() {
console.log(this.name);
}
function Son() {
this.age = 19
}
Son.prototype = new Father();
Son.prototype = {
sayAge: function() {
console.log(this.age)
}
}
var xiaoming = new Son();
xiaoming.sayName(); // Uncaught TypeError: xiaoming.sayName is not a function
使用對象字面量的方式到導致Father和Son之間的關係被切斷,因此最後調用sayName()
方法就會報錯。
總結
以上就是我對原型和原型鏈的理解,原型鏈雖然強大,但它也存在一些問題,最主要的問題就是包含引用類型值的原型屬性會被所有實例共用,本文只是對原型和原型鏈進行分析,關於繼承問題不在本文的討論範圍之內,這是cc的第一篇文章,也算是小白文章,如寫得不好請指出,我會及時更正,謝謝大家~