英文原文: Javascript: An Exploration of Prototypal Inheritance
在Javascript面向?qū)ο缶幊讨?,原型繼承不僅是一個重點也是一個不容易掌握的點。在本文中,我們將對Javascript中的原型繼承進(jìn)行一些探索。
我們先來看下面一段代碼,
//構(gòu)造器函數(shù)
function Shape(){
this.x = 0;
this.y = 0;
}
//一個shape實例
var s = new Shape();
雖然這個例子非常簡單,但是有四個非常重要的點需要在此闡明:
s
是一個對象,并且默認(rèn)的它擁有訪問Shape.prototype
(即每個由Shape
構(gòu)造函數(shù)創(chuàng)建的對象擁有的原型)的權(quán)限;簡單來說,Shape.prototype
就是一個監(jiān)視著所有Shape
實例的對象。你可以將一個對象的原型想象成一個由許多屬性(變量/函數(shù))組成的后備集合,當(dāng)原型在它自己身上找不到東西時就會去原型中查找。Shape
實例中共享。例如,所有的原型都擁有(直接)訪問原型的權(quán)限。this
的值都是指向用來調(diào)用函數(shù)的這個實例。因此如果我們調(diào)用了一個s
中的函數(shù),如果這個函數(shù)并沒有在s
中直接定義而是在s
的原型中,this
值依然指向s
。現(xiàn)在我們將上面強調(diào)的幾點運用到一個例子中。假設(shè)我們將一個函數(shù)getPosition()
綁定到s
上。我們可能會這樣做:
s.getPosition = function() {
return [this.x, this.y];
};
這樣做沒有什么錯誤。你可以直接調(diào)用s.getPosition()
然后你將獲得返回的數(shù)組。
但是如果我們創(chuàng)建了另一個Shape
的實例s2
怎么辦呢,它依然能夠調(diào)用getPosition()
函數(shù)嗎?
答案顯然是不能。
getPosition
函數(shù)直接在實例s
中被創(chuàng)建。因此,這個函數(shù)并不會存在與s2
中。
當(dāng)你調(diào)用s2.getPosition()
時,下面的步驟會依次發(fā)生(注意第三步非常重要):
s2
會檢查getPosition
的定義;s2
中;s2
的原型(和s
一起共享的后備集合)檢查getPosition
的定義;一個簡單(但并不是最優(yōu))的解決方案是將getPosition
在實例s2
(以及后面每一個需要getPosition
的實例)中再定義一次。這是一個很不好的做法因為你在做無意義的復(fù)制代碼的工作,而且在每個實例中定義一個函數(shù)會消耗更多的內(nèi)存(如果你關(guān)心這點的話)。
我們有更好的辦法。
我們完全可以達(dá)到所有實例共享getPosition
函數(shù)的目的,不是在每個實例中都定義getPosition
,而是定義在構(gòu)造器函數(shù)的原型中。我們來看下面的代碼:
//構(gòu)造器函數(shù)
function Shape(){
this.x = 0;
this.y = 0 ;
}
Shape.prototype.getPosition = function(){
return [this.x, this.y];
};
var s = new Shape(),
s2 = new Shape();
由于原型在所有Shape
的實例中共享,s和s2都能夠訪問到getPosition
函數(shù)。
調(diào)用s2.getPosition()
函數(shù)會經(jīng)歷下面的步驟:
s2
檢查getPosition
的定義;s2
中;getPosition
的定義存在于原型中;getPosition
會連同指向s2
的this
一起執(zhí)行;綁定到原型的屬性非常適合于重用。你可以在所有的實例中重用同樣的函數(shù)。
當(dāng)你把對象或者數(shù)組綁定到原型中的時候要非常小心。所有的實例將會共享這些被綁定的對象/數(shù)組的引用。如果一個實例操縱了對象或數(shù)組,那么所有的實例都會受到影響。
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.types = ['round', 'flat'];
s = new Shape();
s2 = new Shape();
s.types.push('bumpy');
console.log(s.types); // ['round', 'flat', 'bumpy']
console.log(s2.types); // ['round', 'flat', 'bumpy']
當(dāng)s.types.push(’bumpy’)
這行代碼運行時,實例s將會檢查一個叫做types
的數(shù)組。它不存在與實例s
中,于是原型檢查這個數(shù)組。這個數(shù)組,types
,存在于原型中,因此我們?yōu)樗砑右粋€元素bumpy
。
結(jié)果,由于s2
也共享原型,它也能通過非直接的方式發(fā)現(xiàn)types
數(shù)組發(fā)生了變化。
一個真實的例子就是當(dāng)你使用Backbone.js
時也會發(fā)生類似的事情。當(dāng)你定義了一個視圖/模型/集合,Backbone
會把你通過extend
函數(shù)(例如:Backbone.View.extend({})
)傳遞的屬性添加到你定義的實體的原型中。
這意味著如果你在定義實體時添加了一個對象或者數(shù)組,所有的實例將會共享這些對象或者數(shù)組,很有可能你的一個實例會毀掉另外一個實例。為了避免這樣的情況,你經(jīng)常會看到人們將這些對象/數(shù)組包含在一個函數(shù)中,每次返回一個對象/數(shù)組的實例。
注意:Backbone
在model defaults的部分中談到了這一點:
記住在JavaScript中,對象是以引用的方式被傳遞的,因此如果你包含了一個對象作為默認(rèn)值,它將在所有實例中被共享。因此,我們將defaults定義為一個函數(shù)。
假設(shè)現(xiàn)在我們想要創(chuàng)建一種特定類型的Shape
,比如說一個圓。如果它能繼承Shape
的所有功能并且還能在它的原型中定義自定義函數(shù)那該多好:
function Shape() {
this.x = 0;
this.y = 0;
}
function Circle() {
this.radius = 0;
}
那么我們怎么形容一個circle
是一個shape
呢?有以下幾種方法:
當(dāng)我們創(chuàng)建一個圓時,我們想要讓實例擁有一個半徑(來源于Circle構(gòu)造函數(shù)),以及一個x位置,一個y位置(來源于Shape構(gòu)造函數(shù))。
我們我們僅僅聲明c = new Circle()
,那么c
僅僅只有半徑。Shape
構(gòu)造函數(shù)對x
和y
進(jìn)行了初始化。我們想要這個功能。因此我們來借用這個功能。
function Circle() {
this.radius = 0;
Shape.call(this);
}
最后一行代碼Shape.call(this)
調(diào)用了Shape
構(gòu)造函數(shù)并改變了當(dāng)Circle
構(gòu)造函數(shù)被調(diào)用時指向this
的值。
好吧,這到底是在說些什么呢?
現(xiàn)在我們來使用上面的構(gòu)造函數(shù)創(chuàng)建一個新的圓然后看看發(fā)生了什么:
var c = new Circle();
這行代碼調(diào)用了Circle
構(gòu)造函數(shù),它首先在c
上綁定了一個變量radius
。記住,此時的this
指向的是c
。我們接著調(diào)用Shape
構(gòu)造函數(shù),然后將Shape
中的this
值指向當(dāng)前在Circle
中的this
值,也就是c
。Shape
構(gòu)造函數(shù)將x
和y
綁定到了當(dāng)前的this
上,也就是說,c
現(xiàn)在擁有值為0的x
和y
屬性。
另外,你在這個例子中放置Shape.call(this)
的位置并不重要。如果你想在初始化之后重載x和y(也就是將圓心放在一個另外的地方),你可以在調(diào)用Shape函數(shù)之后完成這件事。
問題是現(xiàn)在我們實例化的圓雖然擁有了變量x
,y
和radius
,但是它并不能從Shape
的原型中獲取任何東西。我們需要設(shè)置Circle
構(gòu)造函數(shù)來將Shape
的原型重置為它的原型 – 以便所有的圓都能獲取作為shape
的福利。
一種方式是我們將Circle.prototype
的值設(shè)置為Shape.prototype
:
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.getPosition = function () {
return [this.x, this.y];
};
function Circle() {
this.radius = 0;
Shape.call(this);
}
Circle.prototype = Shape.prototype; // 注意看這里
var s = new Shape(),
c = new Circle();
這樣做運行的很好,但是它并不是最優(yōu)選擇。實例c
現(xiàn)在擁有訪問getPosition
函數(shù)的權(quán)限,因為Circle
構(gòu)造器函數(shù)和Shape
構(gòu)造器函數(shù)共享了它的原型。
要是我們還想給所有的Cirecle
定義一個getArea
函數(shù)怎么辦?我們將把這個函數(shù)綁定到Circle
構(gòu)造器函數(shù)的原型中以便它可以為所有圓的實例所用。
編寫下面的代碼:
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.getPosition = function () {
return [this.x, this.y];
};
function Circle() {
this.radius = 0;
Shape.call(this);
}
Circle.prototype = Shape.prototype;
Circle.prototype.getArea = function () {
return Math.PI * this.radius * this.radius;
};
var s = new Shape(),
c = new Circle();
現(xiàn)在的情況是Circle
和Shape
共享同一個原型,我們在Circle.prototype
中添加了一個函數(shù)其實也就相當(dāng)于在Shape.prototype
中添加了一個函數(shù)。
怎么會這個樣子!!
一個Shape
的實例并沒有radius
變量,只有Circle
實例擁有radius
變量。但是現(xiàn)在,所有的Shape
實例都可以訪問getArea
函數(shù) – 這將導(dǎo)致一個錯誤,但是當(dāng)所有圓調(diào)用這個函數(shù)時則一切正常。
將所有的原型設(shè)置為同一個對象并不能滿足我們的需求。
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.getPosition = function () {
return [this.x, this.y];
};
function Circle() {
this.radius = 0;
}
Circle.prototype = new Shape();
var c = new Circle();
這個方法非常的酷。我們并沒有借用構(gòu)造器函數(shù)但是Circle
擁有了x
和y
,同時也擁有了getPosition
函數(shù)。它是怎么實現(xiàn)的呢?
Circle.prototype
現(xiàn)在是一個Shape
的實例。這意味著c
有一個直接的變量radius
(由Circle構(gòu)造器函數(shù)提供)。然而,在c
的原型中,有一個x
和y
。現(xiàn)在注意,有趣的東西要來了:在c
的原型的原型中,有一個getPosition
函數(shù)的定義。看起來其實是這樣的:
因此,如果你試圖獲取c.x
,那么它將在c
的原型中被找到。
這種方法的缺點是如果你想要重載x
和y
,那么你必須在Circle
構(gòu)造器或者Circle
原型中做這件事。
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.getPosition = function () {
return [this.x, this.y];
};
function Circle() {
this.radius = 0;
}
Circle.prototype = new Shape();
Circle.prototype.x = 5;
Circle.prototype.y = 10;
var c = new Circle();
console.log(c.getPosition()); // [5, 10]
調(diào)用c.getPosition
將會經(jīng)歷下列步驟:
c
中沒有被找到;c
的原型(Shape
的實例)中沒有被找到;Shape
實例的原型(c
的原型的原型)中被找到;c
的this
一起被調(diào)用;getPosition
函數(shù)的定義中,我們在this
中尋找x
;x
沒有直接在c
中被找到;c
的原型(Shape
實例)中查找x
;c
的原型中找到x
;c
的原型中找到y
;除了有一層一層的原型鏈帶來的頭痛之外,這個方法還是很不錯的。
這個方法還可以使用Object.create()
來替代。
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.getPosition = function () {
return [this.x, this.y];
};
function Circle() {
this.radius = 0;
Shape.call(this);
this.x = 5;
this.y = 10;
}
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle; // 這一步是將Circle的原型的構(gòu)造函數(shù)重置為Circle
var c = new Circle();
console.log(c.getPosition()); // [5, 10]
這個方法的一大好處就是x
和y
直接被綁定到了c
上 – 這將使查詢速度大大提高(如果你的程序關(guān)心這件事情)因為你再也不需要向上查詢原型鏈了。
我們來看一看Object.create
的替代方法(polyfill):
Object.create = (function() {
// 中間構(gòu)造函數(shù)
function F() {}
return function(o) {
...
// 將中間構(gòu)造函數(shù)的原型設(shè)置為我們給它的對象o
F.prototype = o;
// 返回一個中間構(gòu)造函數(shù)的實例;
// 它是一個空對象但是原型是我們給它的對象o
return new F();
};
})();
上說過程基本上是完成了Circle.prototype = new Shape()
;只是現(xiàn)在Circle.prototype
是一個空對象(一個中間構(gòu)造函數(shù)F的實例),而它的原型是Shape.prototype
。
非常重要的一點是,如果你在Shape
構(gòu)造函數(shù)上綁定有對象/數(shù)組,那么所有的圓都可以修改這些共享的對象/數(shù)組。如果將Circle.prototype
設(shè)置為一個Shape
的實例時這個方法會有很大的缺陷。
function Shape() {
this.x = 0;
this.y = 0;
this.types = ['flat', 'round'];
}
Shape.prototype.getPosition = function () {
return [this.x, this.y];
};
function Circle() {
this.radius = 0;
}
Circle.prototype = new Shape();
var c = new Circle(),
c2 = new Circle();
c.types.push('bumpy');
console.log(c.types); // ["flat", "round", "bumpy"]
console.log(c2.types); // ["flat", "round", "bumpy"]
為了避免這種情況的發(fā)生,你可以借用Shape
的構(gòu)造函數(shù)并且使用Object.create
以便每一個圓都能擁有它自己的types
數(shù)組。
...
function Circle() {
this.radius = 0;
Shape.call(this);
}
Circle.prototype = Object.create(Shape.prototype);
var c = new Circle(),
c2 = new Circle();
c.types.push('bumpy');
console.log(c.types); // ["flat", "round", "bumpy"]
console.log(c2.types); // ["flat", "round"]
我們現(xiàn)在在前面討論的基礎(chǔ)上更進(jìn)一步,創(chuàng)建一個新的Circle
的類型,Sphere
。一個橢圓,和圓差不多,只是在計算面積時有不同的公式。
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.getPosition = function () {
return [this.x, this.y];
};
function Circle() {
this.radius = 0;
Shape.call(this);
this.x = 5;
this.y = 10;
}
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.getArea = function () {
return Math.PI * this.radius * this.radius;
};
function Sphere() {
}
// TODO: 在這里設(shè)置原型鏈
Sphere.prototype.getArea = function () {
return 4 * Math.PI * this.radius * this.radius;
};
var sp = new Sphere();
我們應(yīng)該使用哪種方法來設(shè)置原型鏈?記住,我們并不想要毀掉我們關(guān)于圓的getArea
的定義。我們只是想在橢圓中有另一種方式的實現(xiàn)。
我們不能夠借用構(gòu)造函數(shù)并為原型賦值,因為這樣做將會改變所有圓的getArea
的定義。然而,我們可以使用Object.create
或者將Sphere
的原型設(shè)置為一個Circle
的實例。我們來看看應(yīng)該怎么做:
...
function Circle() {
this.radius = 0;
Shape.call(this);
this.x = 5;
this.y = 10;
}
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.getArea = function () {
return Math.PI * this.radius * this.radius;
};
function Sphere() {
Circle.call(this);
}
Sphere.prototype = Object.create(Circle.prototype);
Sphere.prototype.getArea = function () {
return 4 * Math.PI * this.radius * this.radius;
};
var sp = new Sphere();
調(diào)用sp.getArea()
將會經(jīng)歷一下步驟:
sp
中查找getArea
的定義;sp
中沒有找到相關(guān)定義;Sphere
的原型(一個中間對象,它的原型是Circle.prototype
)中查找;getArea
的定義,由于我們在Sphere
的原型中重新定義了getArea
,這里采用新的定義;sp
的this
調(diào)用getArea
方法;我們注意到Circle.prototype
也有一個getArea的定義。然而,由于Sphere.prototype
已經(jīng)有了一個getArea
的定義,我們永遠(yuǎn)不會使用到Circle.prototype
中的的getArea
– 這樣我們就成功的重載了這個函數(shù)(重載意味著在查詢鏈的前面定義了一個名字相同的函數(shù))。
End. All rights reserved @gejiawen
.
更多建議: