js实现继承的六种方式
继承,顾名思义就是子类通过一定的方式拥有父类的属性和方法,一个简单的栗子:有一个孩子,他拥有一个很有钱父亲,他的父亲不仅有钱,还有很多的人脉,那么他想要得到他父亲的钱和人脉来让自己变得有钱和强大,就必须通过自己的方式(比如努力)去继承他父亲拥有的这些,而靠自己去实现这些则会变得困难,因为需要从0开始,而js的继承也是如此。
因此,我们必须先拥有一个父类,来提供继承的属性和方法
继承的目的就是拿到父类的属性和方法,在理解的过程中,应该带着这一理念进行思考,只是实现的方式不同,各有优缺点
定义父类
function Person(name) {
this.name = name || '->f';
this.sayName = function() {
console.log(this.name);
}
}
Person.prototype.age = 25;
Person.prototype.sayAge = function() {
console.log(this.age);
}
一.原型链继承
特点:将父类的实例作为子类的原型
function Sun(name) {
this.name = name || '->sun';
}
Sun.prototype = new Person();
var s = new Sun('sun');
console.log(s.name); // sun
s.sayName(); // f
console.log(s.age) // 25
s.age(); // 25
console.log(s instanceof Person); // true
console.log(s instanceof Sun); // true
优缺点
优点:
特别纯粹又简单的继承关系,实例既是子类的实例,也是父类的实例,父类新增加的方法、属性,子类都可以访问到
缺点:
想要给子类新增属性和方法,必须在给它原型赋值父类实例【Sun.prototype = new Person()】之后进行,一个最直接的表现就是在之前定义的属性或者方法可能会被来自父类的属性或方法覆盖
无法实现多继承,因为原型一次只能被一个实例更改
来自原型的所有属性被所有实例共享
创建子类实例时,无法向父类构造函数传参
二.构造继承
特点:复制父类构造器中的属性和方法给子类
function Sun(name) {
Person.call(this);
this.name = name || '->sun';
}
var s = new Sun('sun');
console.log(s.name); // sun
s.sayName(); // sun
console.log(s.age); // undefined
console.log(s instanceof Person); // false
console.log(s instanceof Sun); // true
s.sayAge(); // 抛出错误
优缺点
优点:
解决了子类共享父类引用属性的问题
子类实例可以向父类传参
可以实现多继承,比如使用call去指向多个父类
缺点:
实例只是子类的实例
只能继承父类实例的属性和方法,不能继承其原型上的属性和方法
无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
三.实例继承
特点:为父类实例添加新特性,作为子类实例的返回
function Sun(name) {
var p = new Person();
p.name = name || 'sun';
return p;
}
var s = new Sun('sun');
console.log(s.name); // sun
s.sayName(); // sun
console.log(s.age) // 25
s.sayAge(); // 25
console.log(s instanceof Person); // true
console.log(s instanceof Sun); // false
优缺点
优点:
不限制调用的方式,不管是new 子类,或者子类(),返回的对象是一致的,因为实例化,只要构造函数返回的是对象,都会作为实例化后对象,直接调用,本身就有返回结果,因此是一致的
缺点:
实例是父类的实例,不是子类的实例
不支持多继承,因为一次只能返回一个父类的实例对象
四.拷贝继承
function Sun(name) {
var p = new Person();
for(var key in p) {
Sun.prototype[key] = p[key];
}
Sun.prototype.name = name || '->sun';
}
var s = new Sun('sun');
console.log(s.name); // sun
s.sayName(); // sun
console.log(s.age); // 25
s.sayAge(); // 25
console.log(s instanceof Person); // false
console.log(s instanceof Sun); // true
优缺点
优点:
支持多继承
缺点:
效率低,性能差,占用内存高,因为需要拷贝父类属性
无法获取父类不可枚举的属性,因为不可枚举的属性,for-in无法获取到
五.组合继承
特点:复制父类的构造,继承父类的属性和方法,保留了传参的优点,然后将父类实例作为子类原型,实现函数复用
function Sun(name) {
Person.call(this);
this.name = name || '->sun';
}
Sun.prototype = new Person();
Sun.prototype.constructor = Sun;
var s = new Sun('sun');
console.log(s.name); // sun
s.sayName(); // sun
console.log(s.age); // 25
s.sayAge(); // 25
console.log(s instanceof Person); // true
console.log(s instanceof Sun); // true
优缺点
优点:
弥补了构造继承的缺点,现在既可以继承实例的属性和方法,也可以继承原型的属性和方法
既是子类的实例,也是父类的实例
不存在引用属性共享问题
可传参
函数可复用
缺点:
调用了两次父类构造函数,生成两份实例(子类实例将子类原型上的那份屏蔽了)
六.寄生组合继承
特点:过滤掉父类的实例属性,避免了组合继承生成两份实例的缺点
function Sun(name) {
Person.call(this);
this.name = name || '->sun';
}
(function() {
var Sub = function() {};
Sub.prototype = Person.prototype;
Sub.prototype = new Sub();
Sun.prototype.constructor = Sun;
})();
var s = new Sun('sun');
console.log(s.name); // sun
s.sayName(); // sun
console.log(s.age); // 25
s.sayAge(); // 25
console.log(s instanceof Person); // true
console.log(s instanceof Sun); // true
优缺点
优点:
接近完美
缺点:
实现起来较为复杂