继承的本质就是复制,即重写原型对象,代之以一个新类型的实例
原型链
假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针,假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是所谓原型链的概念
继承思想:利用原型让一个引用类型继承另一个引用类型的属性和方法
核心:子类的原型等于父类的实例
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
以上代码定义了两个类型:SuperType 和 SubType。每个类型分别有一个属性和一个方法,他们的主要区别是 SubType 继承了 SuperType,而继承是通过创建 SuperType 的实例,并将该实例赋给 SubType.prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了
记住:所有的引用类型默认都继承了 Object,而这个继承也是通过原型链实现的,所有函数的默认原型都是 Object 的实例,因为默认原型都会包含一个内部指针,指向 Object.prototype,这也正是所有自定义类型都会继承 toString()、valueOf() 等默认方法的根本原因
谨慎的定义方法
子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
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
第一个方法 getSubValue() 被添加到了 SubType() 中,第二个方法 getSuperValue() 是原型链已经存在的一个方法,但重写这个方法将会屏蔽原来的那个方法。换句话说,当通过 SubType 的实例调用 getSuperValue() 时,调用的就是这个重新定义的方法;但通过 SuperType 的实例调用 getSuperValue() 时,还会继续调用原来的那个方法。这里要格外注意的是,必须在用 SuperType 的实例替换原型之后,在定义这两个方法
还有一点需要提醒读者,即在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做会重写原型链,如下面的例子
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;
},
someOtherMethod : function(){
return false;
}
}
var instance = new SubType();
console.log(instance.getSuperValue()); // Uncaught TypeError: instance.getSuperValue is not a function
原型链的问题
原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型。想必大家还记得,包含引用类型值的原型属性会被所有实例共享,而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。下列代码可以用来说明这个问题。
function SuperType(){
this.colors = ['red', 'blue', 'green'];
}
function SubType(){}
// 继承了 SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors) // red,blue,green,black
var instance2 = new SubType();
console.log(instance2.colors) // red,blue,green,black
原型链的第二个问题是,在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链
借用构造函数
在子类型构造函数的内部调用超类型的构造函数
别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用 apply() 和 call() 也可以在(将来)新创建的对象上执行构造函数
继承思想:在子类型的构造函数的内部调用超类型构造函数,通过 apply,call 方法实现
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"]
通过使用 apply() 和 call() ,实际上是在新创建的 SubType 实例的环境下调用了 SuperType 构造函数,这样一来,就会在新 SubType 对象上执行
SuperType() 函数中定义的所有对象初始化代码。结果 SubType 的所有实例都会有自己的 colors 属性
1.传递参数
相对于原型链而言,借用构造函数有很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数
function SuperType(name){
this.name = name;
}
function SubType(){
// 继承了 SuperType,同时还传递了参数
SuperType.call(this, "summer");
// 实例属性
this.age = 20;
}
var instance1 = new SubType();
console.log(instance1.name); // summer
console.log(instance1.age); // 20
在 SubType 构造函数内部调用 SuperType 构造函数时,实际上是为 SubType 的实例设置了 name 属性。
为了确保 SuperType 构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加子类型独有的属性。
2 、借用构造函数的问题
如果仅仅是调用构造函数,那么也将无法避免构造函数模式存在问题-----方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言是不可见的,结果,所有类型都只能使用构造函数模式,考虑到这些问题,借用构造函数的技术也是很少单独使用的
组合继承
核心思想:使用原型链实现对原型属性和方法的继承,使用借用构造函数来实现对实例属性的继承
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,拥有 name、colors 属性,第二次调用 SuperType()
this.age = age; // 自定义实例属性
}
// 继承方法
SubType.prototype = new SuperType(); // 原型链,继承原型属性和方法,第一次调用 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
在这个例子中,SuperType 构造函数定义了两个属性:name 和 colors,SuperType 的原型定义了一个方法 sayName()。
SubType 构造函数在调用 SuperType 构造函数时传入了 name 参数,紧接着又定义了它自己的属性 age。然后,将 SuperType 的实例赋值给 SubType 的原型,然后又在该新原型上定义了方法 sayAge()。
这样一来,就可以让两个不同的 SubType 实例既分别拥有自己的属性-----包括 colors 属性,又可以使用相同的方法了。
组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为 javascript 中最常用的继承模式
缺点:无论什么情况下,都会调用两次超类型构造函数,一次是在创建子类型原型的时候,一次是在子类型构造函数内部
原型式继承
继承思想:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型
function object(o){
function F(){};
F.prototype = o;
return new F();
}
在 object() 函数的内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个实例
ver 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');
console.log(person.friends); // Shelby, Court, Van, Rob, Barbie
这种原型式继承,要求你必须有一个对象可以作为另一个对象的基础
ES6 通过新增 Object.create() 方法规范了原型式继承,这个方法接受两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象,在传入一个参数的情况下 Object.create() 与 object() 方法的行为相同
寄生式继承
继承思想:寄生式继承式与原型式继承紧密相关的一种思路,寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象
function object(o){
function F(){};
F.prototype = o;
return new F();
}
function createAnother(original){
var clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function(){ // 以某种方式来增强这个对象
alert('hi')
}
return clone; // 返回这个对象
}
ver person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // hi
缺点:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类似
寄生组合式继承
前面说过,组合继承是 JavaScript 最常用的继承模式,不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数,一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性
在第一次调用 SuperType 构造函数时 SubType.prototype 会得到两个属性: name 和 colors; 它们都是 SuperType 的实例属性,只不过现在位于 SubType 的原型中。当调用 SubType 构造函数时,又会调用一次 SuperType 构造函数,这一次又在新对象上创建了实例属性 name 和 colors。于是,这两个属性就屏蔽了原型中的两人同名属性
有两组 name 和 colors 属性,一组在实例上,一组在 subType 原型中。这就是调用两次 SuperType 构造函数的结果。好在我们已经找到了解决这个问题方法-----寄生组合式继承
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是,不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}
这个示例中的 inheritPrototype 函数实现了寄生组合式继承的最简单形式。这个函数接收两个参数,子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性,最后一步,将新创建的对象(即副本) 赋值给子类型的原型。这样,我们就可以用调用 inhertPrototype() 函数的语句,去换前面例子中为子类型原型赋值的语句了
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()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式