说起类(class)这个概念,对于那些熟悉oo语言的程序员们必然是熟的和葡萄干似的,但是在JS中,类是个很有意思的概念。ES5中,JS主要使用原型和原型链来实现类和继承,而在ES6中则提供了class和extends语法糖来实现类和继承
// ES5原型方式实现类
function SuperTypeES5() {
}
// SuperTypeES5的"类属性"
SuperTypeES5.prototype.sayName = function () {
this.name = 'Allen';
this.age = 22;
console.log(this.name);
}
// ES6中class方式实现类
class SuperTypeES6 {
constructor(name, age) {
this.name = name || 'Allen';
this.age = age || 22;
}
}
ES6使用class关键字对于学习过oo语言的开发人员是很好理解的,所以本篇文章主要介绍下ES5中关于原型的类和继承的几种模式。
创建对象
先介绍ES5中几种创建对象的方式
- 工厂模式:把给对象赋值的工作抽象封装起来,然后在返回这个对象。就比如你把一个干干净净的对象交给一个工厂,然后工厂给你加工(增强对象),加工完后再还给你。
function Person(name, age) {
let o = new Object();
o.name = name;
o.age = age;
o.sayName = function () {
console.log(this.age);
}
return o;
}
let p1 = new Person('job', 12);
工厂模式是一种比较简单,容易理解的模式,但是在对象识别的问题上有致命的缺陷。因为每个对象都是返回的都是o
,所以每个对象只能被识别为Object而不能识别为Person。而构造函数模式就解决了这个问题。
- 构造函数模式
function PersonConstructorModel(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name)
};
}
let p2 = new PersonConstructorModel('Allen', 22);
构造函数模式就很好的解决了对象识别的问题。但是有个问题,对于sayName这个公共方法,通过构造函数模式创建对象会在每一个对象上都会定义一次。因为函数在JS中是应用类型,所以每个函数都会占内存。这样与我们抽象的初衷就背道而驰了。所以我们使用了原型模式。
- 原型模式(还有一种比较简洁的原型模式,使用那种方式需要重新指定原型的constructor的指向,但核心思想没什么差别,在这里就不赘述了)
function PersonPrototypeModel() {
}
PersonPrototypeModel.prototype.name = 'Allen';
PersonPrototypeModel.prototype.age = 22;
PersonPrototypeModel.prototype.friends = ['Tom', 'Jerry'];
PersonPrototypeModel.prototype.sayName = function () {
console.log(this.name);
};
let p3 = new PersonPrototypeModel();
通过这样的方式就可以借助原型链的思想来实现对一类对象的属性的抽象了。每一个PersonPrototypeModel实例化的对象都共享name = 'Allen'
,age=22
,friends = ['Tom', 'Jerry']
以及sayName四个属性。但这种模式也有个很严重的问题,就是所有对象共享的friends
是一个引用类型。所以你在某个对象上操作friends
时也会导致其他同类对象friends
的改变。所以为了解决这个问题,就衍生出了组合构造函数和原型的模式(使用最广泛,认可度最高的一种模式)。
- 组合构造函数和原型模式
function PersonPrototypeAndCons() {
// 为每一个对象都实例化自己的friedns
this.friends = ['Tom', 'Jerry'];
}
PersonPrototypeAndCons.prototype.name = 'Allen';
PersonPrototypeAndCons.prototype.age = '22';
PersonPrototypeAndCons.prototype.sayName = function () {
console.log(this.name);
};
let p4 = new PersonPrototypeAndCons();
这种模式的核心思想就是引用类型放在构造函数里,然后通过this
来为每一个实例化对象在自己内部实例化一个引用类型的对象(本例中就是friends
)。这样就既解决了封装的问题,又解决了每个对象共享引用类型的问题。
说完对象的几种构造模式我们再讲讲ES5中常用的几种继承方法。我所接触到的继承方法大概有6种:原型链继承,构造函数继承,组合继承,原型式继承,寄生继承,寄生组合式继承。对于这六种方法之间的优缺点有太多介绍的文章了,本人才疏学浅就不过多的介绍了,我就讲讲觉得比较有意思的几个继承方法吧。
- 组合继承(一种集成了原型链继承和构造函数继承优点的方法)
// 组合继承
function CombineInheritSuper(name) {
this.firends = ['rj', 'wy', 'qyf'];
this.name = name;
}
CombineInheritSuper.prototype.sayName = function () {
console.log(this.name, 'zuhe');
}
function CombineInheritSub(name, age) {
CombineInheritSuper.call(this, name);
this.age = age;
}
CombineInheritSub.prototype = new CombineInheritSuper();
let p2 = new CombineInheritSub('Allen', 22);
p2.sayName();
其实组合继承的方式就把子类的原型指向一个父类的实例对象,然后通过子类→子类原型(父类实例)→父类原型的方法来继承父类的属性。然后为了解决子类实例化对象共享父类实例上对象的问题(原型模式建立对象也有这种问题),所以在子类中调用父类的构造函数,这样子类的friends
就会屏蔽父类实例化对象(子类原型)上的friends
。
- 组合寄生继承
// 寄生组合继承
// 核心思想:使用不包含实例属性的原型副本
function inhert(superType, subType) {
let clone = Object.create(superType.prototype);
subType.prototype = clone;
subType.constructor = SubType;
}
function SuperType(name, age) {
this.name = name;
this.age = age;
this.friends = ['rj', 'wl', 'qyf'];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
}
function SubType(name, age, hobby) {
SuperType.apply(this, arguments);
this.hobby = hobby;
}
inhert(SuperType, SubType);
let p = new SubType('Allen', '22', 'base');
console.log(p.name, p.age, p.hobby);
console.log(p.friends);
console.log(SubType.prototype);
p.sayName();
其实组合寄生继承和组合构造函数和原型链的继承方式思想差不多,只不过这种方式少掉用了一次父类的构造函数,也就少实例化了一个父类对象,从而减少了内存占用。这也是一种比较理想的继承方式。