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