许多OO(面向对象)语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,因此在JS中只支持实现继承,而且实现继承主要是依靠原型链来实现的。
在说继承之前,先来复习下原型链的知识:
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
假如现在我们让原型对象等于另一个类型的实例,那么此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着指向另一个构造函数的指针。再假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是原型链的基本概念。
原型和原型链的关系网如下图:
图片来自:https://www.mollypages.org/misc/js.mp
首先看一段代码:
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();
console.log(instance.getSuperValue()); //true
上面的代码定义了两个构造函数SuperType和SubType。每个构造函数分别有一个属性和方法。它们的主要区别就是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。原来存在于SuperType的实例中所有的属性的方法,现在也存在于SubType.prototype中了。在确立了继承关系后,我们又给SuperType.prototype添加了一个方法getSubValue,这样就在继承了SuperType的属性和方法的基础上又添加了一个新的方法。
上面要注意的是instance的constructor现在指向的是SuperType,这是因为SubType的原型指向了另一个对象——SuperType的原型,而这个原型对象的constructor指向的是SuperType。
如果我们要确定一个原型和实例的关系,我们可以采用instanceof操作符和isPrototypeOf()方法。
console.log(instance instanceof Object); //true
console.log(instance instanceof SuperType); //true
console.log(instance instanceof SubType); //true
由于原型链的关系,我们可以说instance是Object、SuperType或SubType中任何一个类型的实例。因此我们使用instanceof操作符返回的都是true。同样的,使用isPrototypeOf()方法也会返回true,代码如下:
console.log(Object.prototype.isPrototypeOf(instance)); //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance)); //true
原型链虽然强大,可以用它来实现继承,但是它也存在一些问题。第一个是引用类型值的原型属性会被所有实例共享,第二个是在创建子类型的实例时,不能像超类型的构造函数中传递参数。因此开发人员发明了其他更好的方法来实现继承。
1.借用构造函数(伪造对象或经典继承)
该方法的主要思想即在子类型的构造函数的内部调用超类型构造函数。
函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在新创建的对象上执行构造函数。
function SuperType() {
this.colors = ["red", "green", "yellow"];
}
function SubType() {
//继承了SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //red,green,yellow,black
var instance2 = new SubType();
console.log(instance2.colors); //red,green,yellow
上面的代码中,我们通过call()(或者使用apply()方法也可以),使SubType继承了SuperType。实际上我们是在新创建的SubType实例的环境下调用了SuperType构造函数。这样一来,每个SubType实例就有属于自己的colors副本,且不影响。
借用构造函数还可以用来传递参数,代码如下:
function Sup(name) {
this.name = name;
}
function Sub() {
//继承了Sup,还传递了参数
Sup.call(this, "yiMu");
//实例属性
this.age = 23;
}
var instance = new Sub();
console.log(instance.name); //yiMu
console.log(instance.age); //23
2.组合继承
组合继承有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。思路是使用原型链继承实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,即通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。
function SuperType(name) {
this.name = name;
this.colors = ["green", "red", "yellow"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
//继承属性
SuperType.call(this, name);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType; //这里一定要手动指向SubType,否则指向的则是SuperType
SubType.prototype.sayAge = function() {
console.log(this.age);
};
var instance1 = new SubType("qtb", 23);
instance1.colors.push("black");
console.log(instance1.colors); //green,red,yellow,black
instance1.sayName(); //qtb
instance1.sayAge(); //23
var instance2 = new SubType("ygl", 22);
console.log(instance2.colors); //green,red,yellow,black
instance2.sayName(); //ygl
instance2.sayAge(); //22
在上面这个例子中,SuperType构造函数定义了两个属性name和colors。SuperType的原型定义了一个方法sayName()。SubType构造函数在调用SuperType构造函数时传入了name参数,紧接着又定义了它自己的属性age。然后将SuperType的实例赋给SubType的原型,又在该原型上定义了新的方法sayAge()。这样一来,就可以让两个不同的SubType实例既分别拥有自己的属性——包括colors属性,又可以使用相同的方法了。
组合继承的优点是避免了原型链继承和借用构造函数继承的缺陷,融合了它们的优点,是JS中最常用的继承模式。
3.原型式继承
原型式继承没有使用严格意义上的构造函数,而是借助原型基于已有的对象创建新的对象,同时还不必因此创建自定义类型。
function object(o) {
function F() {
F.prototype = o;
return new F();
}
}
上面的object()对传入其中的对象进行了一次浅copy。
规范化的原型式继承是使用Object.create()方法。这个方法接受两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下Object.create()和object()方法的行为相同。
var res = {
name: "qtb",
friends: ["A", "B", "C"]
};
var foo1 = Object.create(res);
foo1.name = "ygl";
foo1.friends.push("D");
var foo2 = Object.create(res);
foo2.name = "yiMu";
foo2.friends.push("E");
console.log(res.friends); //A,B,C,D,E
Object.create()的第二参数与Object.defineProperties()方法的第二参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
var per = {
name: "qtb",
friends: ["a", "b", "c"]
};
var foo = Object.create(per, {
name: {
value: "ygl"
}
});
console.log(foo.name); //ygl
支持Object.create()方法的浏览器目前有IE9+、Firefox4+、Safari5+、Opera12+和Chrome。
4.寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
function fool(ori) {
var c = Object(ori); //通过调用函数创建一个新对象
c.sayHi = function() { //以某种方式来增强这个对象
console.log("Hi");
};
return c; //返回这个对象
}
var per = {
name: "qtb",
friends: ["a", "b", "c"]
};
var d = fool(per);
d.sayHi(); //Hi
5.寄生组合式继承
即寄生模式与组合模式相结合的继承模式。其基本模式如下:
function bar(subType, superType) {
var prototype = object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}
用法:
function bar(subType, superType) {
var prototype = object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}
function SuperType(name) {
this.name = name;
this.colors = ["green", "red", "yellow"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
bar(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};
如果你在本文中发现错误或者有异议的地方,可以在评论区留言,谢谢!