淺談Javascript中的原型繼承

2018-06-09 16:18 更新

英文原文: 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)需要在此闡明:

  1. 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ì)去原型中查找。
  2. 原型可以在所有的Shape實(shí)例中共享。例如,所有的原型都擁有(直接)訪問原型的權(quán)限。
  3. 當(dāng)你調(diào)用實(shí)例中的一個(gè)函數(shù)時(shí),這個(gè)實(shí)例會(huì)在它自己身上查找這個(gè)函數(shù)的定義。如果找不到,那么原型將會(huì)查找這個(gè)函數(shù)的定義。
  4. 無論被調(diào)用的函數(shù)的定義在哪里找到(在實(shí)例本身中或者它的原型中),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ā)生(注意第三步非常重要):

  1. 實(shí)例s2會(huì)檢查getPosition的定義;
  2. 這個(gè)函數(shù)不存在于s2中;
  3. s2的原型(和s一起共享的后備集合)檢查getPosition的定義;
  4. 這個(gè)函數(shù)不存在與原型中;
  5. 這個(gè)函數(shù)的定義沒有被找到;

一個(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)歷下面的步驟:

  1. 實(shí)例s2檢查getPosition的定義;
  2. 函數(shù)不存在與s2中;
  3. 檢查原型;
  4. getPosition的定義存在于原型中;
  5. getPosition會(huì)連同指向s2this一起執(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í)例。

注意:Backbonemodel defaults的部分中談到了這一點(diǎn):

記住在JavaScript中,對(duì)象是以引用的方式被傳遞的,因此如果你包含了一個(gè)對(duì)象作為默認(rèn)值,它將在所有實(shí)例中被共享。因此,我們將defaults定義為一個(gè)函數(shù)。

另一種類型的Shape

假設(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呢?有以下幾種方法:

借用構(gòu)造函數(shù)并且賦值給原型

當(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ì)xy進(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ù)將xy綁定到了當(dāng)前的this上,也就是說,c現(xiàn)在擁有值為0的xy屬性。

另外,你在這個(gè)例子中放置Shape.call(this)的位置并不重要。如果你想在初始化之后重載x和y(也就是將圓心放在一個(gè)另外的地方),你可以在調(diào)用Shape函數(shù)之后完成這件事。

問題是現(xiàn)在我們實(shí)例化的圓雖然擁有了變量xyradius,但是它并不能從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)在的情況是CircleShape共享同一個(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ì)象并不能滿足我們的需求。

Circle原型是一個(gè)Shape的實(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è)方法非常的酷。我們并沒有借用構(gòu)造器函數(shù)但是Circle擁有了xy,同時(shí)也擁有了getPosition函數(shù)。它是怎么實(shí)現(xiàn)的呢?

Circle.prototype現(xiàn)在是一個(gè)Shape的實(shí)例。這意味著c有一個(gè)直接的變量radius(由Circle構(gòu)造器函數(shù)提供)。然而,在c的原型中,有一個(gè)xy?,F(xiàn)在注意,有趣的東西要來了:在c的原型的原型中,有一個(gè)getPosition函數(shù)的定義??雌饋砥鋵?shí)是這樣的:

因此,如果你試圖獲取c.x,那么它將在c的原型中被找到。

這種方法的缺點(diǎn)是如果你想要重載xy,那么你必須在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)歷下列步驟:

  1. 該函數(shù)在c中沒有被找到;
  2. 該函數(shù)在c的原型(Shape的實(shí)例)中沒有被找到;
  3. 該函數(shù)在Shape實(shí)例的原型(c的原型的原型)中被找到;
  4. 該函數(shù)連同指向cthis一起被調(diào)用;
  5. getPosition函數(shù)的定義中,我們?cè)?code>this中尋找x
  6. x沒有直接在c中被找到;
  7. 我們?cè)?code>c的原型(Shape實(shí)例)中查找x;
  8. 我們?cè)?code>c的原型中找到x;
  9. 我們?cè)?code>c的原型中找到y;

除了有一層一層的原型鏈帶來的頭痛之外,這個(gè)方法還是很不錯(cuò)的。

這個(gè)方法還可以使用Object.create()來替代。

借用構(gòu)造函數(shù)并使用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è)方法的一大好處就是xy直接被綁定到了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。

你應(yīng)該使用哪個(gè)方法

非常重要的一點(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"]

一個(gè)更高級(jí)的例子

我們現(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)歷一下步驟:

  1. sp中查找getArea的定義;
  2. sp中沒有找到相關(guān)定義;
  3. Sphere的原型(一個(gè)中間對(duì)象,它的原型是Circle.prototype)中查找;
  4. 在這個(gè)中間對(duì)象中找到關(guān)于getArea的定義,由于我們?cè)?code>Sphere的原型中重新定義了getArea,這里采用新的定義;
  5. 連同指向spthis調(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.


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)