es5时代js面向对象的的编程需要重点关注的问题就是“继承”,要实现继承,在js中有多种方式,虽然各种方式都有利弊,没有做好的但是相较之下总有最优的,本篇博文将这几种方法逐一列举总结,参考来源《javascript高级程序设计》。
先准备一个父类:
// 定义一个person类
function Person (name) {
// 属性
this.name = name || 'Mr';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Person.prototype.work = function(work) {
console.log(this.name + '的工作是:' + work);
};
1.原型链继承
将构造函数的原型设置为另一个构造函数的实例对象,这样就可以继承另一个原型对象的所有属性和方法,可以继续往上,直到Object,最终形成原型链。每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。关于显式原型prototype与隐式原型的关系:
1、所有的引用类型(数组、函数、对象)可以自由扩展属性(除null以外)。
2、所有的引用类型都有一个’_ _ proto_ _'属性(也叫隐式原型,它是一个普通的对象)。
3、所有的函数都有一个’prototype’属性(这也叫显式原型,它也是一个普通的对象)。
4、所有引用类型,它的’_ _ proto_ _'属性指向它的构造函数的’prototype’属性。
5、当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,那么就会去它的’_ _ proto_ _'属性(也就是它的构造函数的’prototype’属性)中去寻找。
代码实现:
/**
*
* 1.原型链实现继承
* 将父类的实例作为子类的原型
*
**/
function Stu(){ }
Stu.prototype = new Person();
Stu.prototype.name = '张明';
// Test Code
var stu = new Stu();
console.log(stu.name);
stu.work('学生');
stu.sleep();
console.log(stu instanceof Person);
console.log(stu instanceof Stu);
观察原型链:
特点:简单,易于实现
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
- 父类新增原型方法/原型属性,子类都能访问到
缺点:
- 无法实现多继承
- 来自原型对象的引用属性是所有实例共享的,一处修改全部更改。
- 创建子类实例时,无法向父类构造函数传参
2.构造函数继承
使用父类的构造函数来增强子类实例,即复制父类的实例属性给子类(没用到原型)
function Teacher(name){
Person.call(this);
this.name = name || '李老师';
}
// Test Code
var teacher = new Teacher("王刚");
console.log(teacher.name);
teacher.work('老师');
teacher.sleep();
console.log(teacher instanceof Person); // false
console.log(teacher instanceof Teacher); // true
输出:
特点:
- 子类实例可以共享父类引用属性
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象)
缺点:
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
3.实例继承
/**
*
* 3.实例继承
*给父类实例添加新特性,作为子类实例返回
*/
function Engineer(name){
var engineerObj = new Person()
engineerObj.name = name || '刘工';
return engineerObj;
}
var engineer = new Engineer();
// Test Code
engineer.work('工程师');
engineer.sleep();
console.log(teacher instanceof Person); // false
console.log(teacher instanceof Teacher); // true
结果:
特点:
不限制调用方式,不管是new 子类()还是子类直接调用(),返回的对象具有相同的效果。
缺点:
- 实例是父类的实例,不是子类的实例
- 不支持多继承
4.拷贝继承
/**
*
* 4.拷贝继承
*将父类原型上的属性方法拷贝给子类
*/
function Lawyer(name){
var per = new Person();
for(var p in per){
Lawyer.prototype[p] = per[p];
}
Lawyer.prototype.name = name || 'Tom';
}
// Test Code
var lawyer = new Lawyer("章三");
lawyer.work('工程师');
lawyer.sleep();
console.log(lawyer instanceof Person); // false
console.log(lawyer instanceof Teacher); // true
结果:
特点:
支持多继承
缺点:
- 效率较低,内存占用高(因为要拷贝父类的属性)
- 无法获取父类不可枚举的方法
5.组合继承
/**
*
* 5.组合继承
*1,2的组合
*通过调用父类构造函数,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
*/
function Doctor(name){
Person.call(this);
this.name = name || 'Tom';
}
Doctor.prototype = new Person();
// 注意更改构造函数指向
Doctor.prototype.constructor = Doctor;
// Test Code
var doctor = new Doctor("王主任");
doctor.work('医生');
doctor.sleep();
console.log(doctor instanceof Person); // true
console.log(doctor instanceof Doctor); // true
结果:
特点:
- 结合了1和2优点,可以继承实例属性/方法,也可以继承原型属性/方法
既是子类的实例,也是父类的实例 - 不存在引用属性共享问题
- 可传参
- 函数可复用
缺点:
调用了两次父类构造函数,生成了两份实例
6.寄生组合继承
/**
*
* 6.寄生组合继承
*5的优化
*通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
*/
function Programmer(name){
Person.call(this);
this.name = name || 'Tom';
}
(function(){
// 创建一个没有实例方法的类
var Super = function(){};
Super.prototype = Person.prototype;
//将实例作为子类的原型
Programmer.prototype = new Super();
})();
Programmer.prototype.constructor = Programmer; // 需要修复下构造函数
// Test Code
var programmer = new Programmer("gcc");
programmer.work('程序员');
programmer.sleep();
console.log(programmer instanceof Person); // true
console.log(programmer instanceof Programmer); // true
结果:
注意:如果没有Programmer.prototype.constructor = Programmer;
构造方法指向父类
如果加上Programmer.prototype.constructor = Programmer;
综上寄生组合式继承才是最有选择。