js中的继承主要是依靠原型链来实现的。下面我们来分析一下继承的方式还有其优缺点。
原型链继承
首先介绍一下原型链的基本概念。
先来理解一下原型、构造函数和实例的关系。
- 每个构造函数都有一个原型对象(通过prototype属性)
- 原型对象都包含一个指向构造函数的指针(通过constructor属性)
- 实例都包含一个指向原型对象的内部指针(通过隐式proto属性)
那么,若原型对象等于另一个原型的实例,则此时原型对象将包含一个指向另一个原型对象的指针。相应的另一个原型中也包含一个指向另一个构造函数的指针。如此层层递进,就构成了实例与原型的链条。
这个概念可能不好理解,下面看来自高级程序上的例子:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = false;
}
SubType.prototype = new SuperType(); // 继承的关键
SubType.prototype.getSubValue = function(){
return this.subproperty;
}
var instance = new SubType();
alert(instance.getSuperValue()); // true
例子中通过令SubType的原型对象等于SuperType的实例,将SubType与SuperType关联起来。使得SubType的实例可以访问SuperType的属性。
确定原型和实例的关系有两种方法:(它们都是通过原型链层层查找来确定)
- instanceof
- isPrototypeOf()
使用原型链继承的方法有两个缺点:
- 由于包含引用类型值的原型属性会被所有实例共享,这样容易导致修改一个实例的引用值另一个也会被修改。
先看一个例子:function SuperType() { this.colors = ["red","blue","green"]; this.name = "super"; } function SubType() { } SubType.prototype = new SuperType(); var instance1 = new SubType(); instance1.colors.push("black"); instance1.name = "sub"; console.log(instance1.name); // sub console.log(instance1.colors); //red, blue, green, black var instance2 = new SubType(); console.log(instance2.name); // super console.log(instance2.colors); //red, blue, green, black
就像上面的例子所示,当我们new一个新对象时(其过程可查看new一个新对象会发生什么),原型属性会复制一份到我们实例中。对于值类型,实例会复制其名字和值放在另一块内存中;而对于引用类型,实例只是复制了指向它的值的指针。因而修改实例的值类型,不会影响其他实例;但是修改引用类型的值,其他实例也会被影响到。
- 创建子类型的实例时,并不能在不影响所有对象实例的情况下给超类型的构造函数传递参数。
构造函数继承
也叫伪造对象或经典继承
基本思想:在子类型构造函数内部调用超类型构造函数。
还是先看例子:
function SuperType(name) {
this.colors = ['red', 'black', 'blue'];
this.name = name;
}
function SubType(name) {
SuperType.call(this, name);
}
SuperType.prototype.getName = function () {
console.log(this.name);
};
var instance1 = new SubType('instance1');
instance1.colors.push('green');
var instance2 = new SubType('instance2');
console.log(instance1.colors); // red,black,blue,green
console.log(instance2.colors); // red,black,blue
console.log(instance1.name); // instance1
console.log(instance2.name); // instance2
instance1.getName(); // error
instance2.getName(); // error
可以看出,使用构造函数继承的方式解决了原型链继承的问题。
- 实例可以独享一份引用类型的值
通过call改变this指向,这样每次执行SuperType函数,this指向的都是新的对象。相当于每个新的对象都有一份完整的SuperType代码。即每个实例都有一份自己的colors属性副本。 - 可以传参数
我们可以通过call函数向SuperType传参数
构造函数继承也有缺点:
- 无法实现函数复用
看上面的代码可知,我们只是执行了SuperType函数,但是并没有继承它的原型链上的函数。这样会导致若要使用公有函数时,自己定义或者在SuperType构造函数中定义,违背了函数复用的初衷。
###组合继承
也叫伪经典继承,它是最常用的的继承方式。
组合继承将原型链继承和构造函数继承结合到一起。
基本思想是:使用原型链实现对原型属性和方法的继承,借用构造函数实现对实例属性的继承。
看例子:
function SuperType(name){
this.name = name;
this.colors = ['red','blue','green'];
}
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.prototype.sayAge = function () {
console.log(this.age);
};
var instance1 = new SubType("instance1", 29);
instance1.colors.push('black');
console.log(instance1.colors); // red,blue,green,black
instance1.sayName(); //instance1
instance1.sayAge(); // 29
var instance2 = new SubType("instance2", 27);
console.log(instance2.colors); // red,blue,green
instance2.sayName(); // instance2
instance2.sayAge(); // 27
由例子可知,组合继承结合了原型链继承和构造函数继承的优点,既可以拥有属于自己的属性,也有了共同的方法。
当然,它也有缺点。
- 无论什么情况下,都会调用两次超类型构造函数。一次在创建子类型的原型时;一次在子类型构造函数内部。当第二次调用时,会重写第一次调用时获得的原型属性。
原型式继承
基本思想:借助原型可以基于已有的对象创建新对象,不必因此创建自定义类型。
看例子:
var person = {
name: 'Nicholas',
friends: ["Shelby","Court","Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // Shelby,Court,Van,Rob,Barbie
console.log(anotherPerson.friends);
// Shelby,Court,Van,Rob,Barbie
console.log(yetAnotherPerson.friends);
// Shelby,Court,Van,Rob,Barbie
可以看出该方法与原型链继承类似,但是写法比它简单。
因此,若只是想让一个对象和另一个相似,可以使用这种方法。
不过它也存在缺点:
- 包含引用类型值的属性始终共享相应的值。
寄生式继承
基本思想:创建一个仅用于封装继承过程的函数,在内部对对象做相关操作,然后返回。
var person = {
name: 'Nicholas',
friends: ["Shelby","Court","Van"]
};
function createAnother(original) {
var clone = Object.create(original);
clone.sayHi = function () {
console.log("Hi");
};
return clone;
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // Hi
这样做使anotherPerson对象不仅有了person的属性方法,还有了自己的方法。但是这样做相当于构造函数那样,并不是真正的函数复用。而且包含引用类型值的属性依然始终共享相应的值。
寄生组合式继承
这个方法属于比较完美的方法。先看代码:
function SuperType(name) {
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function () {
console.log(this.age);
};
这个方法解决了组合继承调用两次超类型的缺点。
首先回顾一下组合继承的两次调用:
- 创建子类型的原型对象时调用(new SuperType())
这个过程会主要是new一个对象的过程,它会复制SuperType的属性和方法给子类型。 - 在子类型构造函数里调用
在构造函数里调用时,又会复制一遍超类型的属性,因而会影响性能。
对于寄生组合式继承方式:
- 先将超类型中的原型对象复制一份,再new对象作为子类型的原型对象。这样做,我们只是复制了超类型的原型对象,而对于超类型构造函数里的属性不会复制。因而减少了调用超类型的次数。
- 这样做仍然保持原型链不变
ES6中class实现继承
class SuperType {
constructor(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
sayName(){
console.log(this.name);
}
}
class SubType extends SuperType {
constructor(name,age){
super(name);
this.age = age;
}
sayAge(){
console.log(this.age);
}
}
let instance1 = new SubType("instance1",25);
instance1.colors.push('black');
console.log(instance1.colors); //red,blue,green,black
instance1.sayName(); // instance1
let instance2 = new SubType("instance2",23);
console.log(instance2.colors); // red,blue,green
ES6关于class的语法不再详细描述。具体可查看ES6 class语法