天天看點

JS 之原型與原型鍊

寫在前面

原型和原型鍊一直都是 JavaScript 中很重要的概念,了解它們有助于我們了解預定義引用類型間的關系以及 JavaScript 中對象繼承的實作機制,下面是我對原型和原型鍊的了解和總結。

原型

原型對象了解

函數對象的 prototype 屬性

我們建立的每一個函數都有一個 prototype 屬性,這個屬性是一個指針,指向一個對象。這個對象的用途是包含可以由特定類型的所有執行個體共享的屬性和方法,簡單來說,該函數執行個體化的所有對象的__proto__的屬性指向這個對象,它是該函數所有執行個體化對象的原型。

function Person(){

}

// 為原型對象添加方法
Person.prototype.sayName = function(){
    alert(this.name);
}      

下面我們來看一下它們之間的關系。

JS 之原型與原型鍊

constructor 屬性

當函數建立,prototype 屬性指向一個原型對象時,在預設情況下,這個原型對象将會獲得一個 constructor 屬性,這個屬性是一個指針,指向 prototype 所在的函數對象。

拿前面的一個例子來說 Person.prototype.constructor 就指向 Person 函數對象。

下面我們來更新一下它們之間的關系圖。

JS 之原型與原型鍊

對象的 proto 屬性

當我們調用構造函數建立一個新執行個體後,在這個執行個體的内部将包含一個指針,指向構造函數的原型對象,在 ECMA-262 第五版中管這個指針叫做 [[Prototype]] 。所有的對象都含有 [[Prototype]] 屬性,指向它們的原型對象,這是了解原型鍊的一個關鍵概念。

需要注意的是在腳本中沒有标準的方式通路 [[Prototype]] 屬性,但是在 Firefox,Safari 和 Chrome 中每個對象中都支援一個屬性 proto 來通路,為了區分 prototype 屬性,我們在下邊都使用 __proto__來表示。

根據前面的 Person 構造函數我們建立一個執行個體

var student = new Person();

console.log(student.__proto__ === Person.prototype); // true      

從上面我們可以看出,這個連接配接是存在與執行個體與構造函數的原型對象之間的,而不是存在于執行個體和構造函數之間的。

下面我們來看一下現在這幾個對象之間的關系

JS 之原型與原型鍊

isPrototypeOf()

雖然我們在腳本中沒有辦法通路到[[Prototype]]屬性,但是我們可以通過 isPrototypeOf 方法來确定對象之間是否存在這種關系。

isPrototypeOf() 方法用于測試一個對象是否存在于另一個對象的原型鍊上。

console.log(Person.prototype.isPrototypeOf(student)); // true

Object.getPrototypeOf()

在 ECMAScript 5 中新增了一個方法叫 Object.getPrototypeOf() ,這個方法可以傳回[[Prototype]]的值,如下

console.log(Object.getPrototypeOf(student) === Person.prototype); // true      

原型屬性

屬性通路

每當代碼讀取對象的某個屬性時,首先會在對象本身搜尋這個屬性,如果找到該屬性就傳回該屬性的值,如果沒有找到,則繼續搜尋該對象對應的原型對象,以此類推下去。

因為這樣的搜尋過程,是以我們如果在執行個體中添加一個屬性時,這個屬性就會屏蔽原型對象中儲存的同名屬性,因為在執行個體中搜尋到該屬性後就不會再向後搜尋了。

屬性判斷

既然一個屬性既可能是執行個體本身的,也有可能是其原型對象的,那麼我們該如何來判斷呢?

在屬性确認存在的情況下,我們可以使用 hasOwnProperty() 方法來判斷一個屬性是存在與執行個體中,還是存在于原型中。注意這個方法隻有在給定屬性存在于執行個體中時,才會傳回 true 。

function Person() {};

Person.prototype.name = "laker" ;

var student = new Person();

console.log(student.name); // laker
console.log(student.hasOwnProperty("name")); // false


student.name = "xiaoming";
console.log(student.name); //xiaoming 屏蔽了原型對象中的 name 屬性
console.log(student.hasOwnProperty("name")); // true      

上面主要是針對屬性确認存在的情況下,如果這個屬性不一定存在的話,這樣判斷就不夠準确,是以我們需要首先判斷這個屬性是否存在,然後再進行上面的判斷操作。

判斷一個屬性是否存在,我們可以使用 in 操作符,它會在對象能夠通路給定屬性時傳回 true,無論該屬性存在于執行個體還是原型中。

是以我們可以封裝這樣一個函數,來判斷一個屬性是否存在于原型中。

function hasPrototypeProperty(object, name){
    return !object.hasOwnProperty(name) && (name in object);
}      

for-in 循環

在使用 for-in 循環時,傳回的是所有能夠通過對象通路的、可枚舉的屬性,其中包括了存在于執行個體中的屬性,也包括了存在于原型中的屬性。

需要注意的一點是,屏蔽了執行個體中不可枚舉屬性的執行個體屬性也會在 for-in 循環中傳回。

所有屬性擷取

如果想要獲得對象上所有可枚舉的執行個體屬性,可以使用 Object.keys() 方法,這個方法接收一個對象作為參數,傳回一個包含所有可枚舉屬性的字元串數組。

如果想要擷取所有的執行個體屬性,無論它是否可以枚舉,我們可以使用 Object.getOwnPropertyNames() 方法。

原型鍊

原型鍊了解

ECMAScript 中描述了原型鍊的概念,并将原型鍊作為實作繼承的主要方法。其基本思想是利用的一個引用類型繼承另一個引用類型的屬性和方法。

原型鍊的主要實作方法是讓構造函數的 prototype 對象等于另一個類型的執行個體,此時的 prototype 對象因為是執行個體,是以将包含一個指向另一個原型的指針,相應地另一個原型中也包含着一個指向另一個構造函數的指針。假如另一個原型又是另一個類型的執行個體,那麼上述關系依然成立,如此層層遞進,就構成了執行個體與類型的鍊條。這就是原型鍊的基本概念。

下面我們來看一個例子。

function Super(){};function Middle(){};function Sub(){};Middle.prototype = new Super();Sub.prototype = new Middle();var suber = new Sub();      

下面我們來看看這幾個對象間的關系

JS 之原型與原型鍊

預設的原型

其實我們上面這個原型鍊是不完整的,還記得我們以前說過所有的引用類型都繼承了 Object 嗎?這個繼承就是通過原型鍊來實作的。我們一定要記住,所有函數的預設原型都是 Object 的執行個體,是以預設原型都會包含一個内部指針,指向 Object.Prototype 。這也正是所有的自定義類型都會繼承 toString() 、valueOf() 等預設方法的根本原因。

那麼我更新一下我們上面的原型鍊。

JS 之原型與原型鍊

Object.prototype 就是原型鍊的終點了,我們可以試着列印一下 Object.prototype.proto,我們會發現傳回的是一個 null 空對象,這就意味着原型鍊的結束。

函數也是對象

其實上面的原型鍊還少了一部分,為什麼呢。因為我們知道在 JavaScript 中,函數也是對象,那麼函數也應該有它的原型對象。下面我們通過一個執行個體來了解它們之間的關系。

執行個體代碼:

function Foo(who) {
this.me = who;
}

Foo.prototype.identify = function() {
return "I am " + this.me;
};

function Bar(who) {
Foo.call( this, who );
}

Bar.prototype = Object.create( Foo.prototype );

Bar.prototype.speak = function() {
alert( "Hello, " + this.identify() + "." );
};

var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );

b1.speak();
b2.speak();      

我們先看一下不包含函數原型的以上執行個體所包含的原型鍊的關系圖:

JS 之原型與原型鍊

繼續閱讀