不是所有的js框架都有類的概念, Douglas Crockford在他的Classical Inheritance in JavaScript中討論了基於類的對象模型。這是一個非常精彩的關於如何實現js繼承的討論。後來,他寫了另一篇文章Prototypal Inheritance in JavaScript,他的結論是:不使用類對象模型,僅通過原型,也能完整的實現js繼承。
至於爲什麼js類庫都提基於類的對象模型?具體的原因也許只有作者自己才清楚。大概一些人喜歡模仿他原來喜歡的語言的對象模型。Prototype受了Ruby的嚴重影響,提供了Class來封裝自己的代碼。實際上,Prototype也僅是自己框架本身在使用Class。這篇文章我將介紹基於原型的繼承和基於類的繼承,並且通過類來實現面向對象的js。這些類會在我們的框架turing.js中使用。
類和原型
一些語言把一切都作爲對象來處理,數字也是對象,字符串也是對象,類也是對象。但是不要把所有的對象都作爲類的實例,我們是面向對象的編程不是面向類的對象。js正的需要類嗎?如果你是Java或Ruby程序員,你會驚訝發現js根本沒有class這個關鍵詞。沒關係,如果需要的話,我們可以自己構建一些類似的功能。
原型的使用
基於原型的繼承看上去是這樣的:function Vector(x, y) {
this.x = x;
this.y = y;
}
Vector.prototype.toString = function() {
return 'x: ' + this.x + ', y: ' + this.y;
}
v = new Vector(1, 2);
// x: 1, y: 2
如果你沒有接觸過js對象編程,頭幾行可能讓你感到奇怪。我定義了一個叫Vector的函數,然後用new Vector()調用它。實際上是創建了一個新的對象然後調用函數Vector,this就用來指向新的對象。
prototype屬性用來擴展實例的方法。通過這種方式,如果你已經實例化了一個對象,然後再prototype裏添加新的屬性,以前的實例也會加上同樣的屬性。有意思吧?
Vector.prototype.add = function(vector) {
this.x += vector.x;
this.y += vector.y;
return this;
}
v.add(new Vector(5, 5));
// x: 6, y: 7
基於原型的繼承
js實現繼承沒有一個固定的方式,如果我們想通過繼承Vector創建一個Point類,可以像下面這樣:function Point(x, y, colour) {
Vector.apply(this, arguments);
this.colour = colour;
}
Point.prototype = new Vector;
Point.prototype.constructor = Point;
p = new Point(1, 2, 'red');
p.colour;
// red
p.x;
// 1
通過使用apply,Point可以執行Vector的構造函數。你可能想知道prototype.constructor是怎麼來的。constructor這是一個prototype的屬性,用來指向創建這個對象的函數。
當你創建一個你自己的對象,你同時也從Object繼承了一些方法,如toString和hasOwnProperty:
p.hasOwnProperty('colour');
// true
基於原型還是基於類
通過原型實現繼承有好幾種方式,我們需要對這些方式進行封裝,然後提供一個統一的接口。這樣可以保持代碼的簡潔性和可讀性。像上面這樣把js的繼承分幾行寫,在視覺上顯得非常混亂。我們最好通過明確的開頭和結尾把代碼包起來。我們這個框架也一樣,通過類可以把它包裝起來。
類的設計
上面的代碼在Prototype是這樣的:Vector = Class.create({
initialize: function(x, y) {
this.x = x;
this.y = y;
},
toString: function() {
return 'x: ' + this.x + ', y: ' + this.y;
}
});
Point = Class.create(Vector, {
initialize: function($super, x, y, colour) {
$super(x, y);
this.colour = colour;
}
});
我們現在用自己的代碼來實現這些功能。我們把代碼要提供的功能點列一下:
- 通過拷貝給類繼承新的方法
- 類的創建:使用apply和prototype.constructor運行構造函數。
- 決定是否繼承自某個父類的能力
- 封裝這些功能
繼承
你可能注意到,extend在Prototype中頻繁的使用,它所做的工作就是在不同的prototype之間進行方法的拷貝。這是我們瞭解prototype如何使用的最好的途徑,其實原理和我們想象的一樣簡單:for (var property in source)
destination[property] = source[property];
類的創建
我們使用create方法來創建一個新類。該方法要像之前的例子一樣,讓類有繼承的能力。// This would be defined in our "oo" namespace
create: function(methods) {
var klass = function() { this.initialize.apply(this, arguments); };
// Copy the passed in methods
extend(klass.prototype, methods);
// Set the constructor
klass.prototype.constructor = klass;
// If there's no initialize method, set an empty one
if (!klass.prototype.initialize)
klass.prototype.initialize = function(){};
return klass;
}