JavaScript面向对象编程之继承

本文详细探讨了JavaScript中的面向对象编程中的继承概念,包括原型链、借用构造函数、组合继承、原型式继承、寄生式继承和寄生组合式继承等。通过实例解释了各种继承方式的优缺点,如原型链可能导致的引用属性共享问题,借用构造函数无法复用方法,以及组合继承调用两次父类构造函数等。最后,提出了寄生组合式继承作为最理想的继承范式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景知识

继承是OOP中非常有名的概念(但不是必要的条件)。基本上目前有两种继承方式,接口继承(Java的Interface)和实现继承。接口继承只继承函数签名,实现继承继承实际的方法。而JavaScript没有函数签名,所以只支持实现继承。而JavaScript的实现继承主要是通过原型链来实现的。

原型链

JavaScript是利用原型来让一个引用类型继承另一个引用类型的属性和方法。
我们简单回顾一下构造函数、原型和实例的关系:
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
原型链的思想就是让原型对象等于另一个类型的实例。层层递进

function SuperType(){
     this.property = true;
}
SuperType.prototype.getSuperValue = function(){
     return this.property;
};
function SubType(){
     this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
     return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true

上图是各个对象之间的关系。另外,我们知道每个对象都有constructor属性,因为是继承下来的。而现在instance.constructor指向SuperType。原型链的搜索模式和之前的原型搜索机制一样。

默认的原型

实际上,我们上图的原型链缺少一环。也就是所有引用类型默认继承的Object,这个继承也是通过原型链来实现的。比如,所有函数的默认原型都是Object的实例。这也是所有自定义类型都会继承toString()和valueOf()等默认方法的根本原因。下面是完整的原型链。

确定原型和实例的关系

通常有两种方式来确定原型和实例的关系。第一个使用instance操作符,第二个使用isPrototypeOf()方法。
使用instance操作符

alert(instance instanceof Object); //true
alert(instance instanceof SubType); // true

使用isPrototypeOf()方法(只要是原型链中出现过的原型都可以说是该原型链所派生的实例的原型):

alert(Object.prototype.isPrototypeOf(instance));//true
alert(SubType.prototype.isPrototypeOf(instance));//true

谨慎地定义方法

通常来说,子类型有时候需要重写父类型的某个方法或者定义自己的方法(父类型中不存在)。需要注意的是给原型添加方法的代码一定要在替换原型的语句之后(因为原型链实现的原理):

function SuperType(){
     this.property = true;
}
SuperType.prototype.getSuperValue = function(){
     return this.property;
};
function SubType(){
     this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
//添加新方法
SubType.prototype.getSubValue = function (){
     return this.subproperty;
};
//重写超类型中的方法
SubType.prototype.getSuperValue = function (){
     return false;
};
var instance = new SubType();
alert(instance.getSuperValue()); //false

并且我们在通过原型链实现继承时,不能使用对象字面量创建原型的方法。因为这样做会重写原型链。

//继承了 SuperType
SubType.prototype = new SuperType();
//使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
     getSubValue : function (){
          return this.subproperty;
     },
     someOtherMethod : function (){
          return false;
     }
};
var instance = new SubType();
alert(instance.getSuperValue()); //error!

原型链的问题

最主要的问题来自于包含引用类型值的原型。因为包含引用类型值的原型属性会被所有实例所共享(这是为什么在构造函数中而不是原型对象中定义属性的原因)。

function SuperType(){
     this.colors = ["red", "blue", "green"];
}
function SubType(){
}
//继承了 SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"

因为SubType的原型是SuperType的实例。所以SubType原型包含一个名为colors的引用属性。对instance1的修改会影响到instance2.(但是如果原型中的属性是基本属性。我们在实例中修改这个属性,其实是在实例中新建了和原型中同名的属性,也就是所谓的覆盖)。

第二个问题:创建子类型的实例时,不能像父类型的构造函数中传递参数。更精确的说,是没有办法在不影响所有对象实例的情况下,给父类型的构造函数传递参数。

所以,综上所述,我们很少单独使用原型链。

借用构造函数

由于之前原型中包含引用类型值的问题,开发人员由构造函数模式想出了一种叫做constructor stealing(借用构造函数或经典继承或伪造对象)的技术。基本思想很简单,在子类型的构造函数内部调用父类型的构造函数

function SuperType(){
     this.colors = ["red", "blue", "green"];
}
function SubType(){
     //继承了 SuperType
     SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

我们通过使用call或apply方法来借调父类型的构造函数。实际上是在新创建的SubType的实例的环境下调用了SuperType构造函数,这样在新创建的SubType对象上执行SuperType()函数的所有对象初始化方法。所以,instance1和instance2都具有字节的color属性的副本(实例属性)。

传递参数

借用构造函数方法,可以在子类型构造函数中向父类构造函数传递参数:

function SuperType(name){
     this.name = name;
}
function SubType(){
     //继承了 SuperType,同时还传递了参数
     SuperType.call(this, "Nicholas");
     //实例属性
     this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29

但是为了确保调用父类构造函数不会重写子类型的属性,可以在调用父类型构造函数之后再添加应该在子类型中定义的属性。

借用构造函数的问题

如果仅仅使用借用构造函数,那么创建对象一章节的仅仅使用构造函数的问题同样会出现。方法都在构造函数里面定义,也就没有函数复用了。而且超类型的原型中定义的方法对子类型是不可见的。所以借用构造函数方法也很少单独使用。

组合继承

combination inheritance,也被叫做伪经典继承(将原型链和借用构造函数的技术组合)。实现的思路是使用原型链实现对原型属性和方法的继承,而通过constructor stealing技术实现对实例属性的继承

function SuperType(name){
     this.name = name;
     this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
     alert(this.name);
};
function SubType(name, age){
     //继承属性
     SuperType.call(this, name);
     this.age = age;
}

//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
     alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

组合继承避免了原型链和借用构造函数的缺陷,融合两者之长,是最常用的JS继承模式。

原型式继承

如果只是想让一个对象与另一个对象保持类似的情况下,没有必要兴师动众地创建构造函数。我们可以使用原型式继承。
原型式继承来自JSON的开创者Douglas Crockford 2006年的题为Prototypal Inheritance in JavaScript.这篇文章介绍一种实现继承的方法。其想法是借助原型可以基于已有的对象创建新对象,同时不必因此创建自定义类型:

function object(o){
     function F(){}
     F.prototype = o;
     return new F();
}

本质上讲,object()对传入的对象执行了一次浅复制。

var person = {
     name: "Nicholas",
     friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

相当于创建了两个person对象的副本。而ECMAScript5通过新增Object.create()方法规范化了原型式继承。
该方法接收两个参数:用作新对象原型的对象和一个为新对象定义额外属性的对象(可选)。
在只传入一个参数时,Object.create()和object()方法的行为相同。
第二个参数的格式和Object.defineProperties()方法的第二个参数格式相同,每个属性都是通过自己的描述符去定义的(覆盖原型对象上的同名属性):

var person = {
     name: "Nicholas",
     friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person, {
     name: {
          value: "Greg"
     }
});

alert(anotherPerson.name); //"Greg"

但是包含引用类型值的属性都会共享相应的值,也会有对应的问题。

寄生式继承

寄生式继承(parasitic)与原型式继承紧密相关的一种思路(也是由Douglas crockford推广)。创建一个仅用于封装继承过程的函数,在函数内部增强对象,最后返回新对象:

function createAnother(original){
     var clone = object(original); //通过调用函数创建一个新对象
     clone.sayHi = function(){ //以某种方式来增强这个对象
          alert("hi");
     };
     return clone; //返回这个对象
}
var person = {
     name: "Nicholas",
     friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式(object()函数不是必需的,任何能够返回新对象的函数都适用于此模式
缺点也是函数复用的问题。

寄生组合式继承

前文中的组合继承最大的问题是会调用两次超类型构造函数:

function SuperType(name){
     this.name = name;
     this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
     alert(this.name);
};
function SubType(name, age){
     SuperType.call(this, name); //第二次调用 SuperType()
     this.age = age;
}
SubType.prototype = new SuperType(); //第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
     alert(this.age);
};


解决办法是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。背后思路是:不必为了指定子类型的原型而调用超类型的构造函数,因为我们需要的就是超类型原型的一个副本而已。本质上,就是使用寄生式基层继承超类型的原型,然后将结果指定个妻子类型的原型

YUI的 YAHOO.lang.extend()采用了寄生组合模式

function inheritPrototype(subType, superType){
     var prototype = object(superType.prototype); //创建对象
     prototype.constructor = subType; //增强对象
     subType.prototype = prototype; //指定对象
}

function SuperType(name){
     this.name = name;
     this.colors = ["red", "blue", "green"];
}
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);
};

这样只调用了一次SuperType构造函数,避免在SubType.prototype上创建不必要的多余的属性。与此同时,原型链保持不变(能够正常使用instanceof 和isPrototypeOf()).普遍认为寄生组合式基层是引用类型最理想的继承范式。

本文原创发表于优快云,为BruceYuj最近所学总结,欢迎转载,但请注明出处

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值