javascript拥有强大的prototype机制,它是基于原型和对象的脚本语言。
在javascript中,并没有“类”的概念,所以严格来说它并不是面向对象的语言,但是利用prototype机制我们可以通过构造函数生成对象来模拟面向对象机制:
function Base()
{
Base.prototype.c = function()
{
alert('c');
}
Base.prototype.a = function()
{
alert('a in Base');
}
Base.prototype.toString = function()
{
return ("Base");
}
Base.prototype.base = new Object();
this.d = function()
{
alert('d');
}
}
var a = new Base();
var b = new Base();
上面的代码定义了一个Base“类”并构造了a、b两个实例。但是注意到,虽然表面上是通过类构造实例,但实际上javascript是直接构造对象,这其中的区别在于:两次执行new Base()操作的时候,实际上程序对Base.prototype.*进行了两次赋值。这些赋值操作会带来一些额外的开销,使得javascript比真正面向对象的语言在构造对象的时候效率低一些。但是,这样做也会获得一些灵活性。
function MyClass()
{
if (MyClass.instanceCount == null)
{
MyClass.instanceCount = 1;
}
else MyClass.instanceCount++;
MyClass.prototype.instanceCount = MyClass.instanceCount;
}
var a = new MyClass();
alert(a.instanceCount);
var b = new MyClass();
alert(b.instanceCount);
alert(a.instanceCount);
上面的代码在构造对象的时候动态改变类和对象(而不仅仅是类!)的instanceCount属性,所以,当构造了两个对象之后,a.instanceCount和b.instanceCount属性的值都为2。
ObjList = new Object();
ObjList.Member = ListMember;
ObjList.MemberCount = 0;
function ListMember(obj)
{
if (ListMember.prototype.head == null)
{
ListMember.prototype.head = this;
}
if (ListMember.prototype.end != null)
{
this.prev = this.end;
this.prev.next = this;
}
ListMember.prototype.end = this;
this.object = obj;
ObjList.MemberCount++;
}
var newMember = new ObjList.Member(new Object(1));
newMember = new ObjList.Member(new Object(2));
newMember = new ObjList.Member(new Object(3));
alert(newMember.object);
alert(newMember.prev.object);
alert(newMember.head.object);
alert(ObjList.MemberCount);
这是一段比较有趣的代码,通过一个全局唯一的对象ObjList的构造方法Member()构造出新的成员,并且将这个新成员直接以链表的形式添加到ObjList的表尾。注意到一旦构造出一个新的成员,所有对象的end属性(而不是类的)都将发生改变。从这段代码也可以看出prototype和this的区别。在构造对象时,对prototype属性的赋值不会分配新的内存单元,除非该属性的值为空。而对this的赋值则会分配新的内存单元给当前对象。因此改变prototype引用的属性将使得生存期内的所有该类对象的属性发生改变,而改变this引用的属性将仅仅改变当前对象的属性。
function MC2(val)
{
this.a = val;
MC2.prototype.b = val;
}
var t1 = new MC2("x");
alert(t1.a); //x
alert(t1.b); //x
var t2 = new MC2("y");
alert(t1.a); //x
alert(t1.b); //y
alert(t2.a); //y
alert(t2.b); //y
上面的代码看出,由于t1.b和t2.b实际上都引用了MC2.prototype.b属性,所以当t2构造的时候实际上改变了MC2.prototype的值,从而使得t1.b的值也发生了改变。因此,当类的属性在运行期需要随着对象改变时,应当用this定义,反之,当定义常量或者类方法,在运行期不随对象改变时,应当尽量采用prototype以节省空间提高效率。仍然要再次提醒,尽管prototype通常用来定义那些“不易改变”的属性和方法,但是实际上它们仍然可以被改变,并且每一次构造一个新的对象时会被重新赋值。特别需要注意的是,如果在一个类中同时定义了this.x和prototype.x属性,前者将覆盖后者。
利用prototype可以模拟实现对象的继承。
function Base()
{
Base.prototype.c = function()
{
alert('c');
}
Base.prototype.a = function()
{
alert('a in Base');
}
Base.prototype.toString = function()
{
return ("Base");
}
Base.prototype.base = new Object();
}
function Derived()
{
Derived.prototype.a = function()
{
alert('a in Derived');
}
Derived.prototype.toString = function()
{
return ("Derived");
}
Derived.prototype.test = function()
{
alert(this);
}
Derived.prototype.base = new Base();
}Derived.prototype = new Base();
上面的代码中,通过对prototype的赋值,实现了Derived类继承Base类。
var x = new Base();
var y = new Derived();
y.a(); //a in Dervide
alert(y.toString()); //Derived
y.base.a(); //a in Base
alert(y.base.toString()); //Base
alert(y.base.base.toString()); //[object Object]
y.a(), y.toString()覆盖了基类的同名方法,y.base.a(),y.c(),y.base.toString()分别调用了基类的同名方法 ,y.base.base.toString()则调用了Object类的toString方法,因为Object类是所有javascript类的超类。
从上面的例子可以看出,javascript类继承的本质是通过改写prototype属性来模拟,需要注意的是,这样实现继承也会带来一些“副作用”,例如改变了对象中constructor字段的值,当Derived类继承自Base类时,获取Derived对象的constructor属性将返回Base对象的构造函数。当然,这个特性也可以解释为“Derived对象的构造过程中确实调用了Base对象的构造函数”,而事实上这也不是什么大问题,上面代码中尽管c.constructor返回的是Base,但是c instanceof Derived的值仍然是true。然而,除此以外,由于是改写prototype,所以次序上需要注意,在执行顺序上对prototype本身的改写必须出现在最前面。但是,我发现即使将prototype的赋值放在构造函数内的第一行,也不能正常工作。这显得有点怪异。而我的理解是,在构造函数体内部,prototype属性是只读的。所以我的建议是,将对类prototype的赋值放到函数体外(推荐写在结束花括号之后),而对于其他成员的赋值,还是应当写在函数体内部。
最后,再提醒一个值得注意的事情,那就是似乎javascript并不能完全模拟继承机制,因为要实现类属性的继承比较困难,至少目前我还没有比较好的方案。如果哪位达人研究过此类问题,请多多指教,感激不尽。