JavaScript继承方式详解
JavaScript并不是面向对象的语言,它是基于对象的语言。在JavaScript中一切皆是对象。在 JavaScript中创建自定义对象的方法 一文中,我已经介绍了基本的创建自定义类型的方法。那么怎么实现类型与类型之间的继承呢?这就是本文要讲的内容。
JavaScript实现继承的方式主要有两种: 原型链继承和借助构造函数继承
一、原型链继承
原型链继承的主要思想是将父类型的实例赋给子类型的原型,这样子类型就可以通过原型对象的[[prototype]]访问到父类型的所有属性和方法。具体实现方式如下:
function SuperType() { this.property = true; //SuperType 实例属性 property } SuperType.prototype.getSuperValue = function() { return this.property; //SuperType 方法 getSuperValue() } function SubType() { this.subProperty = false; //SubType 实例属性 subProperty }; SubType.prototype = new SuperType(); //将SuperType实例赋给SubType的原型 SubType.prototype.constructor = SubType; SubType.prototype.getSubValue = function() { return this.subProperty; //SubType 添加方法 getSubValue() }; var instance = new SubType(); alert(instance.getSuperValue()); // true
父类型SuperType的实例中有一个内部指针[[prototype]]指向SuperType的原型,而将这个实例赋值给子类型SubType的原型后,instance在搜索属性时,会先搜索自身的属性,然后搜索它的原型SubType.prototype中的属性,包括SuperType的实例属性和后添加自身的原型属性,然后会继续搜索到SuperType.prototype的属性。这样SubType就可以访问到SuperType的全部属性了。也就是实现了继承。这个继承是通过在它们之间建立原型链实现的。
不过原型链继承有一些问题。首先,子类型不能像父类型传递参数,其次,由于父类型的实例属性都变成了子类型的原型属性,那么那些引用类型的属性值就会在子类型的所有实例中共享,这样往往跟我们预期的效果不同。
二、借助构造函数继承
相对于原型中引用类型共用的问题,构造函数就有自己的突出优势,这在之前讲解创建对象方法时也提到过。借助构造函数继承的思想是在子类构造函数中调用父类构造函数。具体实现方法如下:
function SuperType(name) { this.name = name; this.friends = ["Mike", "John"]; } function SubType() { // 调用SuperType构造函数,并为其传递一个参数,设置了SubType的name和friends属性值 SuperType.call(this, "Nicholas"); //为SubType添加属性age this.age = "22"; } var instance1 = new SubType(); alert(instance1.name); // Nicholas instance1.friends.push("Kate"); alert(instance1.friends); // Mike,John,Kate alert(instance1.age); // 22 var instance2 = new SuperType("Jake"); alert(instance2.friends); // Mike,John alert(instance2.name); // Jake
在SubType的构造函数中通过call()方法调用SuperType的构造函数,这样所有SubType的实例都会有一个firends副本,不存在共用的问题。并且可以在调用SuperType构造函数时传入参数。
当然这种方法的缺点也是显而易见的,它无法实现函数的复用。
三、组合继承
组合继承是JavaScript中运用最普遍的继承方式,它集合了原型继承和构造函数继承的优点,用构造函数继承父类型的实例属性,用原型链继承父类型的原型方法。具体方法如下:
function SuperType(name) { //父类的实例属性 this.name = name; this.friends = ["Mike","John"]; } //父类的原型方法(函数) SuperType.prototype.sayName = function() { alert(this.name); }; function SubType(name,age) { //子类的实例属性 // 调用父类的构造函数,继承父类的实例属性name,friends SuperType.call(this, name); this.age = age; // 子类新添加的实例属性 } //将父类的实例赋值给子类的原型,原型链继承父类的原型方法sayName() SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; //子类新添加的方法函数 SubType.prototype.sayAge = function() { alert(this.age); }; var instance1 = new SubType("Nicholas", 22); instance1.friends.push("Kate"); alert(instance1.friends); // Mike,John,Kate instance1.sayAge(); // 22 instance1.sayName(); // Nicholas var instance2 = new SubType("Jake", 25); alert(instance2.friends); // Mike,John instance2.sayAge(); // 25 instance2.sayName(); // Jake
在这个例子中,通过在SubType的构造函数中调用SuperType的构造函数,将父类的name和friends属性添加到子类中。再将一个父类的实例 new SuperType() 赋给子类,实现原型链的连接,从而继承了父类的sayName()方法。
这种方法融合了前两种方法的优点,看似完美,实际上它还是有一个缺点:它会调用两次父类的构造函数,从而使子类的构造函数和原型中都包含父类的实例属性。如上例,第一次调用是在讲父类的实例赋给子类的原型上时,new SuperType(), 这样,子类的原型中就有了父类的实例属性。第二次调用是在创建子类的实例时,new SubType(),由于子类的构造函数中又调用了一次父类的构造函数,所以子类的实例中也会包含父类的实例属性。虽然在查找属性时,实例中的属性会先被找到而覆盖掉原型中的属性,但这样做肯定是对性能有影响的。那么怎么避免这个问题呢?
四、寄生组合式继承
我们首先要知道这两次调用父类的构造函数的真正目的是什么。第一次调用,将父类的实例赋值给子类的实例,我们本质上需要的只有指向父类原型的指针[[prototype]]而已,它使父类和子类通过原型链连接起来,而父类的实例属性我们并不需要,它的继承是通过第二次在子类的构造函数中调用实现的。所以,我们现在只要能想出一个方法,能够获得指向父类原型的指针,并把它添加到子类的原型上,就可以省掉第一次调用了。
道格拉斯•克罗克福德在2006年提出了原型式继承方法,在其中用到了这样一个函数:
function object(o) { function F() {} F.prototype = o; return new F(); }
在这个函数中,首先创建了一个临时的空的构造函数F,然后将参数o赋给F的原型,最后返回一个F的实例。如果把这个函数用在继承的实现中,并将父类的原型对象作为参数传入,就正好满足我们之前的要求:F的原型中保存了父类原型对象的副本,可以将F赋给子类的原型,那么子类就可以访问到父类所有的原型属性了。这个过程可以用下面这个函数包装起来:
function inheritPrototype(subType, superType) { var proto = object(superType.prototype); proto.constructor = subType; subType.prototype = proto; }
在函数内部,先创建一个父类的原型副本proto, 然后为其添加constructor属性,最后将这个副本赋给子类。这样就可以不通过调用父类的构造函数建立原型链。用这个函数代替之前组合继承方式中的为子类原型赋值的语句就可以了。
function SuperType(name) { this.name = name; this.friends = ["Mike","John"]; } SuperType.prototype.sayName = function() { alert(this.name); }; function SubType(name,age) { SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function() { alert(this.age); };
这种寄生组合式继承是目前性能最好的继承方式。