淺談javascript的原型及原型鏈
淺談javascript的原型及原型鏈
這裏,我們列出原型的幾個概念,如下:
- prototype屬性
- [[prototype]]
__proto__
prototype屬性
只要創建了一個函數,就會為該函數創建一個prototype
屬性,指向該函數的原型對象。實例對象是不會擁有該屬性的。
默認情況下,該原型對象
也會獲得一個constructor
屬性,該屬性包含一個指針,指向prototype
屬性所在的函數。
Person.prototype.constructor===Person
[[prototype]]和__proto__
javascript中,不可以訪問的內部屬性都是用[[propertyName]]
[[prototype]]
屬性只能是對象可以擁有的屬性。比如實例化的對象以及原型對象
,而不是構造函數。這個屬性指向擁有其屬性的對象的構造函數的原型對象。註意,此處的構造函數
指的是使用new
方式的構造函數。並不因為更改了原型對象上的constructor
屬性而改變。
__proto__
是個啥呢,就是對[[propertyName]]
的實現。也就是說,你可以在支持該實現的瀏覽器下(FF,chrome,safari),去訪問對象的構造函數的原型對象。比如:
var Person=function(name){
this.name=name;
};
var p1=new Person();
p1.__proto__===Person.prototype;//true
Person.prototype={};
var p2=new Person();
p2.__proto__===Object.prototype;//false
當然,__proto__
只是瀏覽器的私有實現,目前ECMAScript標準實現方法是Object.getPrototypeOf(object)
。
var Person=function(name){
this.name=name;
};
var p1=new Person();
Object.getPrototypeOf(p1)===Person.prototype;//true
Person.prototype={};
var p2=new Person();
Object.getPrototypeOf(p2)===Object.prototype;//false
另外一種判斷實例對象和其原型對象存在指向關系(由實例的[[prototype]]指向其構造函數的原型對象)的方法是:isPrototypeOf
Person.prototype.isPrototypeOf(p1);//true
由於原型對象
也是一個對象,所以,它自然而然也擁有[[prototype]]
屬性。
Javascript
並沒有類繼承模型,而是使用原型對象 prototype
進行原型式繼承。
盡管人們經常將此看做是 Javascript
的一個缺點,然而事實上,原型式繼承比傳統的類繼承模型要更加強大。舉個例子,在原型式繼承頂端構建一個類模型很簡單,然而反過來則是個困難得多的任務。Javascript
是唯一一個被廣泛運用的原型式繼承的語言,所以理解兩種繼承方式的差異是需要時間的。
第一個主要差異就是 Javascript
使用原型鏈來繼承:
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};
function Bar() {}
設置 Bar
的 prototype
為 Foo
的對象實例:
Bar.prototype = new Foo();
Bar.prototype.foo = ‘Hello World‘;
確保 Bar
的構造函數為本身,並新建一個 Bar
對象實例:
Bar.prototype.constructor = Bar;
var test = new Bar();
下面我們來看下整個原型鏈的組成:
test [instance of Bar]
Bar.prototype [instance of Foo]
{ foo: ‘Hello World‘ }
Foo.prototype
{ method: ... }
Object.prototype
{ toString: ... /* etc. */ }
在上面的例子中,對象 test
將會同時繼承 Bar.prototype
和 Foo.prototype
。因此它可以訪問定義在 Foo
中的函數 method
。當然,它也可以訪問屬性 value
。需要提到的是,當 new Bar()
時並不會創建一個新的 Foo
實例,而是重用它原型對象自帶的 Foo
實例。同樣,所有的 Bar
實例都共享同一個 value
屬性。我們舉例說明:
test1 = new Bar();
test2 = new Bar();
Bar.prototype.value = 41;
test1.value //41
test2.value//41
原型鏈查找機制
當訪問一個對象的屬性時,Javascript
會從對象本身開始往上遍歷整個原型鏈,直到找到對應屬性為止。如果此時到達了原型鏈的頂部,也就是上例中的 Object.prototype
,仍然未發現需要查找的屬性,那麽 Javascript
就會返回 undefined
值。
原型對象的屬性
盡管原型對象的屬性被 Javascript
用來構建原型鏈,我們仍然可以值賦給它。但是原始值復制給 prototype
是無效的,如:
function Foo() {}
Foo.prototype = 1; // no effect
這裏講個本篇的題外話,介紹下什麽是原始值:
在 Javascript
中,變量可以存放兩種類型的值,分別是原始值和引用值。
1.原始值
(primitive value)
:
原始值是固定而簡單的值,是存放在棧stack
中的簡單數據段,也就是說,它們的值直接存儲在變量訪問的位置。
原始類型有以下五種型:Undefined, Null, Boolean, Number, String
。
2.引用值
(reference value)
:
引用值則是比較大的對象,存放在堆heap
中的對象,也就是說,存儲在變量處的值是一個指針pointer
,指向存儲對象的內存處。所有引用類型都集成自Object
。
原型鏈性能問題
如果需要查找的屬性位於原型鏈的上端,那麽查找過程對於性能而言無疑會帶來負面影響。當在性能要求必要嚴格的場景中這將是需要重點考慮得因素。此外,試圖查找一個不存在屬性時將會遍歷整個原型鏈。
同樣,當遍歷一個對象的屬性時,所有在原型鏈上的屬性都將被訪問。
總結
理解原型式繼承是寫較為復雜的 Javascript
代碼的前提,同時要註意代碼中原型鏈的高度。當面臨性能瓶頸時要學會將原型鏈拆分開來。此外,要將原型對象 prototype
和原型 __proto__
區分開來,這裏主要討論原型對象 prototype
就不闡述關於原型 __proto__
的問題了。
圖片來自基友 kzloser
圖片說明
1.總共三類對象(藍色大框)
2.實例對象(通過new XX() 所得到的實例),跟原型鏈相關的只有 __proto__
屬性,指向其對應的原型對象 *.prototype
。
3.構造函數對象分原生和自定義兩類。跟原型鏈相關的有 __proto__
屬性,除此之外還有 prototype
屬性。它們的 __proto__
屬性都是指向 Function.prototype
這個原型對象的。prototype
也是指向對應的原型對象。
4.原型對象除了一樣擁有 __proto__
外,也擁有獨有的屬性 constructor
。它的__proto__
指向的都是 Object.prototype
,除了 Object.prototype
本身,它自己是指向 null
。而 constructor
屬性指向它們對應的構造函數對象。
5.原型鏈是基於 __proto__
的。實例只能通過其對應原型對象的 constructor
才能訪問到對應的構造函數對象。構造函數只能通過其對應的 prototype
來訪問相應的原型對象。
原型與原型鏈是javascript裏面最最核心的內容,如果不能理解它們之間的存在關系的話,那麽我們是不能理解這門語言的。
在JS中,主要有兩種創建對象的方法, 分別是對象字面量以及調用構造函數
//對象字面量
var obj1 = {}
//調用構造函數
var obj2 = new Object()
其實上述兩種創建對象的方法,本質上是一樣的,都是JS引擎調用對象的構造函數來新建出一個對象。構造函數本身也是一個普通的JS函數
下面我們來看一個例子
//創建構造函數
function Person(name){
this.name = name
}
//每個構造函數JS引擎都會自動添加一個prototype屬性,我們稱之為原型,這是一個對象
//每個由構造函數創建的對象都會共享prototype上面的屬性與方法
console.log(typeof Person.prototype) // ‘object‘
//我們為Person.prototype添加sayName方法
Person.prototype.sayName = function(){
console.log(this.name)
}
//創建實例
var person1 = new Person(‘Messi‘)
var person2 = new Person(‘Suarez‘)
person1.sayName() // ‘Messi‘
person2.sayName() // ‘Suarez‘
person1.sayName === person2.sayName //true
我們借助上面的例子來理解構造函數-原型-實例,三者之間的關系,主要有幾個基本概念
-
構造函數默認會有一個
protoype
屬性指向它的原型 -
構造函數的原型會有一個
consctructor
的屬性指向構造函數本身, 即Person.prototype.constructor === Person
-
每一個
new
出來的實例都有一個隱式的__proto__
屬性,指向它們的構造函數的原型,即person1.__proto__ === Person.prototype person1.__proto__.constructor === Person
了解了這些基本概念之後,我們再來看看javascript的一些原生構造函數的關系網,看下列的圖
引自stackoverflow
按照我們上面的理解, Oject本身是一個構造函數,它也是一個對象,那麽
Object.__proto__ === Function.prototype
為了方便我們記住上圖,還有幾個需要我們知道的特殊概念:
-
Function
的原型屬性與Function
的原型指向同一個對象. 即Function.__proto__ == Function.prototype
-
Object.prototype.__proto__ === null
-
typeof Function.prototype === ‘function‘
淺談javascript的原型及原型鏈