原型鏈比做用域鏈要好理解的多。程序員
JavaScript中的每一個對象,都有一個內置的_proto_
屬性。這個屬性是編程不可見的(雖然ES6標準中開放了這個屬性,然而瀏覽器對這個屬性的可見性的支持不一樣),它其實是對另外一個對象或者null
的引用。編程
當一個對象須要引用一個屬性時,JavaScript引擎首先會從這個對象自身的屬性表中尋找這個屬性標識,若是找到則進行相應讀寫操做,若沒有在自身的屬性表中找到,則在_proto_
屬性引用的對象的屬性表中查找,如此往復,直到找到這個屬性或者_proto_
屬性指向null
爲止。瀏覽器
這個_proto_
的引用鏈,被稱做原型鏈。性能優化
<!--more-->app
注意,此處有一個性能優化的問題:往原型鏈越深處搜索,耗費的時間越多。函數
JavaScript是一種面向對象的語言,而且能夠進行原型繼承。性能
JavaScript中的函數有一個屬性prototype
,這個prototype
屬性是一個對象,它的一個屬性constructor
引用該函數自身。即:優化
func.prototype.constructor === func; // ==> true
這個屬性有什麼用呢?咱們知道一個,一個函數使用new
操做符調用時,會做爲構造函數返回一個新的對象。這個對象的_proto_
屬性引用其構造函數的prototype
屬性。this
所以這個就不難理解了:prototype
var obj = new Func(); obj.constructor == Func; // ==> true
還有這個:
obj instanceof Func; // ==> true
也是經過查找原型鏈上的constructor
屬性實現的。
構造函數生成的不一樣實例的_proto_
屬性是對同一個prototype
對象的引用。因此修改prototype
對象會影響全部的實例。
之因此子類要加引號,是由於這裏說「類」的概念是不嚴謹的。JavaScript是一門面向對象的語言,可是它跟Java等語言不一樣,在ES6標準出爐以前,它是沒有類的定義的。
可是熟悉Java等語言的程序員,也但願使用JavaScript時,跟使用Java類似,經過類生成實例,經過子類複用代碼。那麼在ES6以前,怎麼作到像以下代碼同樣使用相似"類"的方式呢?
var parent = new Parent("Sam"); var child = new Children("Samson"); parent.say(); // ==> "Hello, Sam!" child.say(); // ==> "Hello, Samson! hoo~~" child instanceof Parent; // ==> true
咱們看到,這裏咱們把構造函數當作類來用。
如下咱們討論一下實現的幾種方式:
結合原型鏈的概念,咱們很容易就能寫出這樣的代碼:
function Parent(name){ this.name = name; } Parent.prototype.say = function(){ console.log("Hello, " + this.name + "!"); } function Children(name){ this.name = name; } Children.prototype = new Parent(); Children.prototype.say = function(){ console.log("Hello, " + this.name + "! hoo~~"); }
這個方式缺點很明顯:做爲子類的構造函數須要依賴一個父類的對象。這個對象中的屬性name
根本毫無用處。
// ... Children.prototype = Parent.prototype; // ...
這樣就不會產生無用的父類屬性了。
然而,這樣的話子類和父類的原型就引用了同一個對象,修改子類的prototype
也會影響父類的原型。
這時候咱們發現:
parent.say(); // ==> "Hello,Sam! hoo~~"
這第一次改進還不如不改。
function F(){ // empty } F.prototype = Parent.prototype; Children.prototype = new F(); // ... parent.say(); // ==> "Hello, Sam!" child.say(); // ==> "Hello, Samson! hoo~~"
這樣一來,修改子類的原型只是修改了F
的一個實例的屬性,並無改變Parent.prototype
,從而解決了上面的問題。
在ES5的時代,咱們還能夠直接這樣:
Children.prototype = Object.create(Parent.prototype);
這裏的思路是同樣的,都是讓子類的prototype
不直接引用父類prototype
。目前的現代瀏覽器幾乎已經添加了對這個方法的支持。(但咱們下面會仍以臨時構造函數爲基礎)
可是細細思考,這個方案仍有須要優化的地方。例如:如何讓父類的構造函數邏輯直接運用到子類中,而不是再從新寫一遍同樣的?這個例子中只有一個name
屬性的初始化,那麼假設有不少屬性且邏輯同樣的話,豈不是沒有作到代碼重用?
使用apply
,實現「方法重用」的思想。
function Children(name){ Parent.apply(this, arguments); // do other initial things }
如今完整的代碼以下:
function Parent(name){ this.name = name; } Parent.prototype.say = function(){ console.log("Hello, " + this.name + "!"); } function Children(name){ Parent.apply(this, arguments); // do other initial things } function F(){ // empty } F.prototype = Parent.prototype; Child.prototype = new F(); Children.prototype.say = function(){ console.log("Hello, " + this.name + "! hoo~~"); }
這就是所謂「聖盃」模式,聽着很高大上吧?
以上就是ES3的時代,咱們用來實現原型繼承的一個近似最佳實踐。
「聖盃」模式依然存在一個問題:雖然父類和子類實例的繼承的prototype
對象不是同一個實例,可是這兩個prototype
對象上面的屬性引用了一樣的對象。
假設咱們有:
Parent.prototype.a = { x: 1}; // ...
那麼即便是「聖盃」模式下,依然會有這樣的問題:
parent.x // ==> 1 child.x // ==> 1 child.x = 2; parent.x // ==>2
問題在於,JavaScript的拷貝不是 深拷貝(deepclone)
要解決這個問題,咱們能夠利用屬性遞歸遍歷,本身實現一個深拷貝的方法。這個方法在這裏我就不寫了。
ES6極大的支持了工程化,它的標準讓瀏覽器內部實現類和類的繼承:
class Parent { constructor(name) { //構造函數 this.name = name; } say() { console.log("Hello, " + this.name + "!"); } } class Children extends Parent { constructor(name) { //構造函數 super(name); //調用父類構造函數 // ... } say() { console.log("Hello, " + this.name + "! hoo~~"); } }
如今瀏覽器對其支持程度還不高。可是這種寫法的確省力很多。讓咱們對將來拭目以待。