Description
- 原型链继承
- class 继承
- 经典继承
- 组合继承
- 原型式继承
- 寄生式继承
- 圣杯继承(寄生组合式继承)
一、原型链继承
function SuperType(){
this.property = true;
}
SuperType.property.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构造函数中定义的
引用类型
的属性(如this.arrNames=["A", "B", "C"]),会被SubType中的所有instance共享,且SubType修改引用类型的时候,因为与SuperType中的引用类型是同一个,所以SuperType的引用类型也会跟着修改:
function SuperType(){
this.arrNames = ["A", "B", "C"];
}
function SubType(){}
SubType.prototype = new SuperType();
var instance1 = new SubType();
var instance2 = new SubType();
instance1.arrNames.push("D");
console.log(instance1.arrNames); //"A", "B", "C", "D"
console.log(instance2.arrNames); //"A", "B", "C", "D"
- 创建子类实例时,无法向超类型的构造函数中传递参数。
二、借用构造函数(经典继承或伪造对象)
在子类的构造函数中通过apply()/call()
调用超类的构造函数,相当于让超类在子类中重新执行一次,使得子类具有超类的属性/方法。
function SuperType(){
this.arrNames = ["A", "B", "C"];
}
function SubType(){
SuperType.call(this);
}
var instance1 = new SubType();
var instance2 = new SubType();
instance1.arrNames.push("D");
console.log(instance1.arrNames); //"A", "B", "C", "D"
console.log(instance2.arrNames); //"A", "B", "C"
优点
- 这样做的好处在于解决了原型链中的两个问题。
问题
- 仅仅得到了
SuperType
的自身属性,在SuperType.prototype
中定义的函数方法无法访问,因此无法进行函数复用。
三、组合继承
组合继承避免了原型链和借用构造函数的缺陷,融合了两者的优点,成为了JavaScript中最常用的继承模式。而且,instanceof
和isPrototypeOf()
也能够用于识别基于组合继承创建的对象。
function SuperType(name){
this.name = name;
this.colors = ["r", "g"];
}
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; // 设置constructor
SubType.prototype.sayAge = function() {
console.log(this.age);
}
var instance1 = new SubType("Nic", 21);
instance1.colors.push("B");
console.log(instance1.colors); //"r", "g", "b"
instance1.sayName(); //"Nic"
instance1.sayAge(); //21
var instance2 = new SubType("Ali", 24);
console.log(instance2.colors); //"r", "g"
instance2.sayName(); //"Ali"
instance2.sayAge(); //24
问题
- 调用两次超类构造函数:
四、原型式继承
可以基于已有的对象创建新对象,同时还不必因此创建自定义类型:
function object(o){
function F(){}
F.prototype = o;
return new F();
}
在object函数内部创建一个临时性的构造函数,将传入的对象作为其原型,返回该类型的新实例。本质上object()对传入的对象执行了一次浅复制。
也就是ES5中的Object.create(instance[, params])
,其参数为用作新对象的原型对象和(可选的)一个为新对象定义额外属性的对象:
var person = {
name: "Nic",
friends: ["A", "B"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Jack"
}
});
console.log(anotherPerson.name); // "Jack"
person.friends.push("C");
console.log(anotherPerson.friends); // ['A', 'B', 'C']
console.log(person.friends); // ['A', 'B', 'C']
适用于在没有必要创建构造函数,只是想让一个对象与另一个对象保持类似的情况下或者浅复制。
五、寄生式继承
类似于寄生构造函数+工厂模式
function createAnother(original){
var clone = Object.create(original);
clone.sayHi = function() {
console.log(this.name);
};
return clone;
}
var person = {
name: "Nic"
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"Nic"
问题
- 对象中添加函数不能复用,上面的
sayHi
函数被写死在createAnother
中无法复用
六、圣杯模式(寄生组合式继承)
解决组合继承模式的调用两次超类构造函数问题,是引用类型最理想的继承方式
原理: 增加一个中间层,增强隔离性,解决子类对父类的污染问题
// 对圣杯模式的封装
var inherit = (function () {
function Buffer () {}
return function (Target, Origin) {
Buffer.prototype = Origin.prototype;
Target.prototype = new Buffer();
Target.prototype.constructor = Target; // 添加constructor属性以备用
Target.prototype.super_class = Origin; // 添加super_class 属性以备用
}
})()
// 使用
function Teacher(){}
Teacher.prototype.name = "Tom";
function Student(){}
inherit(Student, Teacher);
var s = new Student();
var t = new Teacher();
console.log(s, t);
prototype内存结构
function Teacher(){}
Teacher.prototype.name = "Tom";
function Student(){}
var s = new Student();
var t = new Teacher();
上面代码的内存结构如下图
调用inherit(Student, Teacher);
继承之后,内存结构改变如下:
首先是function Buffer () {}
和new Buffer()
后内存的变化
然后是下面语句中prototype
保存地址的改变如下图
Buffer.prototype = Origin.prototype;
Target.prototype = new Buffer();
Target.prototype.constructor = Target; // 添加constructor属性以备用
Target.prototype.super_class = Origin; // 添加super_class 属性以备用
七、Class
类声明和类表达式的主体都执行在严格模式
下。比如,构造函数,静态方法,原型方法,getter和setter都在严格模式下执行。
我们先回顾用函数实现Student
的方法:
function Student(name) {
this.name = name;
}
Student.prototype.hello = function () {
console.log(this.name);
}
如果用新的class
关键字来编写Student
,可以这样写:
class Student {
constructor(name) {
this.name = name;
}
hello() {
console.log(this.name);
}
}
比较一下就可以发现,class
的定义包含了构造函数constructor
和定义在原型对象上的函数hello()
(注意没有function
关键字),这样就避免了Student.prototype.hello = function () {...}
这样分散的代码。
注意: 如果constructor
中也有个hello
函数的话则会执行constructor
中的,不会执行外面这个
最后,创建一个Student
对象是一样的:
var xiaoming = new Student('小明');
xiaoming.hello();
class继承
通过extends来实现:
class PrimaryStudent extends Student {
constructor(name, grade) {
super(name); // 记得用super调用父类的构造方法!
this.grade = grade;
}
myGrade() {
alert('I am at grade ' + this.grade);
}
}
ES6
引入的class
和原有的JavaScript原型继承
有什么区别呢?
实际上它们没有任何区别,class
的作用就是让JavaScript引擎
去实现原来需要我们自己编写的原型链代码
。简而言之,用class的好处就是极大地简化了原型链代码。
如果说非要说有差别的话,this
指针的创建顺序不一样
ES5的继承,实质是先创造子类的实例对象this
,然后再将父类的方法添加到this
上(Parent.apply(this)
),ES6的继承机制完全不同,实质是先创造父类的实例对象this
(所以必须先调用super
方法),然后再用子类的构造函数修改this
;