最近在面试中被问到js的继承,当时回答的不太好,所以今天特别总结一下。
我们先来看一个基于原型链的继承例子
//父类
function Person(){}
//子类
function Student(){}
//继承
Student.prototype = new Person();
我们只要把子类的prototype设置为父类的实例,就完成了继承,也就是js里面的原型链继承。
接下来我们加上一点东西
//父类
function Person(name,age){
this.name = name || 'unknow'
this.age = age || 0
}
//子类
function Student(name){
this.name = name
this.score = 80
}
//继承
Student.prototype = new Person();
var stu = new Student('lucy');
console.log(stu.name); //lucy --子类覆盖父类的属性
console.log(stu.age); // 0 --父类的属性
console.log(stu.score); // 80 --子类自己的属性
基于上面的代码,我们再给父类和子类分别加一个方法
//父类
function Person(name,age){
this.name = name || 'unknow'
this.age = age || 0
}
Person.prototype.say = function(){
console.log('i am a person');
}
//子类
function Student(name){
this.name = name
this.score = 80
}
//继承
Student.prototype = new Person();
Student.prototype.study = function(){
console.log('i am studing');
}
var stu = new Student('lucy');
console.log(stu.name); //lucy --子类覆盖父类的属性
console.log(stu.age); // 0 --父类的属性
console.log(stu.score); // 80 --子类自己的属性
stu.say(); // i am a person --继承自父类的方法
stu.study(); // i am studing --子类自己的方法
到这里就完成了一个原型链继承,是不是觉得很简单。但是原型链继承有一个缺点,就是如果属性是引用类型的话,会共享引用类型,请看下面代码
//父类
function Person(){
this.hobbies = ['music','reading']
}
//子类
function Student(){
}
//继承
Student.prototype = new Person();
var stu1 = new Student();
var stu2 = new Student();
stu1.hobbies.push('basketball');
console.log(stu1.hobbies); // ["music", "reading", "basketball"]
console.log(stu2.hobbies); // ["music", "reading", "basketball"]
我们可以看到,当我们改变stu1的引用类型的属性时,stu2对应的属性也会跟着更改,这就是原型链继承的缺点—引用属性会被所有实例共享。
那我们如何解决这个问题呢?就是下面我们要提到的借用构造函数继承,我们来看一下使用构造函数继承的最简单例子:
//父类
function Person(){
this.hobbies = ['music','reading']
}
//子类
function Student(){
Person.call(this);
}
var stu1 = new Student();
var stu2 = new Student();
stu1.hobbies.push('basketball');
console.log(stu1.hobbies); // ["music", "reading", "basketball"]
console.log(stu2.hobbies); // ["music", "reading"]
这样,我们就解决了引用类型被所有实例共享的问题了
注意:这里跟原型链继承有个比较明显的区别是并没有使用prototype继承,而是在子类里面执行父类的构造函数。相当于把父类的代码复制到子类里面执行一遍,这样做的另一个好处就是可以给父类传参。
//父类
function Person(name){
this.name = name;
}
//子类
function Student(name){
Person.call(this,name);
}
var stu1 = new Student('lucy');
var stu2 = new Student('lili');
console.log(stu1.name); // lucy
console.log(stu2.name); // lili
构造函数解决了引用类型被所有实例共享的问题,但正是因为解决了这个问题,导致一个很矛盾的问题出现了—函数也是引用类型,也没办法共享了。也就是说,每个实例里面的函数,虽然功能一样,但是却不是一个函数,就相当于我们每实例化一个子类,就复制了一遍函数代码。
//父类
function Person(name){
this.say = function() {};
}
//子类
function Student(name){
Person.call(this,name);
}
var stu1 = new Student('lucy');
var stu2 = new Student('lili');
console.log(stu1.say === stu2.say); // false
以上代码说明父类的构造函数,在子类的实例下是不共享的。
总结:
继承方式 | 继承的核心代码 | 优缺点 |
---|---|---|
原型继承 | Student.prototype = new Person() | 实例的引用类型共享 |
构造函数继承 | 在子类(Student)里执行Person.call(this) | 实例的引用类型不共享 |
从上表我们可以看出原型继承和构造函数继承这两种方式的优缺点刚好是互相矛盾的,那么我们有没有方法可以鱼和熊掌兼得呢?
接下来就是组合继承登场了,组合继承就是各取上面2种继承的长处,普通属性使用构造函数继承,函数使用原型链继承。接下来就看代码吧:
//父类
function Person(name){
this.hobbies = ['music','reading'];
}
Person.prototype.say = function(){
console.log('i am a person');
}
//子类
function Student(name){
Person.call(this); //构造函数继承(继承属性)
}
Student.prototype = new Person(); //原型继承(继承方法)
var stu1 = new Student('lucy');
var stu2 = new Student('lili');
stu1.hobbies.push('basketball');
console.log(stu1.hobbies); // ["music", "reading", "basketball"]
console.log(stu2.hobbies); // ["music", "reading"]
console.log(stu1.say === stu2.say); // true
这样我们就既能实现属性的独立,又能做到函数的共享。
至此,我们就把js里面的常用继承了解完了,总结一下:
- 原型链继承,会共享引用属性
- 构造函数继承,会独享所有属性,包括引用属性(重点是函数)
- 组合继承,利用原型链继承要共享的属性,利用构造函数继承要独享的属性,实现相对完美的继承
了解js继承的同学可能知道继承还有其他方式,比如原型式继承、寄生式继承、寄生组合继承等。今天在这里记录的是比较常用的3种继承方式,其他剩余的继承方式以后再学习。