浅谈Javascript中如何实现继承(上)
大家好,我是在国企打工的前端菜鸟小王,在这里和大家进行交流,如有错误还希望向大家指正,今天我们来讨论js中的继承机制。
继承是面向对象编程的一块基石,继承就像人类世界中的传宗接代一样,父母会把自己的一些特点传给儿女,儿女和父母又不完全一致,既有了父母的特点又有了自己新的特点。
在 Java 中通过 extends 关键字可以申明一个类是从另外一个类继承而来的,一般形式如下:
class 父类 {
}
class 子类 extends 父类 {
}
在 ECMAScript 2015 (ES6)之前javascript是没有实现extends关键字从而显式地表达继承,在此之前之前我们一般有两种方式来实现继承关系,分别是①类式继承和②构造函数继承。下面我们对此进行展开讨论:
一、原型链式继承
简单来讲,类式继承就是将父类的实例赋值给子类的原型对象。
// 类式继承
// 声明父类
function SuperClass() {
this.superName = '我是父类'
}
// 为父类添加共有方法
SuperClass.prototype.getSuperClassName = function() {
return this.superName
}
// 声明子类
function SubClass() {
this.subName = '我是子类'
}
// 对父类进行继承
SubClass.prototype = new SuperClass()
// 为子类添加共有方法
SubClass.prototype.getSubClassName = function() {
return this.subName
}
console.log(SubClass.prototype);
上述代码中最关键之处在于我们将父类SuperClass实例化以后赋值给子类SubClass的原型对象。但是我们在末尾打印子类的原型对象发现这时候子类的构造函数已经变成了SuperClass,这样子类实例化时会导致以下两个后果:
- 父类的初始化代码会被执行,包括一些属性的设置和初始化逻辑
- 若父类的构造函数需要参数,那么子类在实例化时也必须提供相应的参数否则报错
因此我们在将子类原型赋值为父类的实例后要修正子类的构造函数constructor
SubClass.prototype = new SuperClass()
// 所有涉及到原型链继承的继承方式都要修改子类构造函数的指向,否则子类实例的构造函数会指向SuperType。
SubClass.prototype.constructor = SubClass;
子类使用父类的方法
let sub_instance = new SubClass()
console.log(sub_instance.getSubClassName()); // 我是子类
console.log(sub_instance.getSuperClassName()); // 我是父类
[!NOTE]
类式继承虽然可以复用父类的方法,但是存在两个很明显的缺陷:
其一,由于子类通过其原型prototype对父类实例化,继承了父类。所以父类中的共有属性要是引用类型,就会在子类中被所有实例所共用,一个子类的实例更改子类原型从父类集成而来的共有属性就会直接影响到其他子类。
function SuperClass() {
this.superName = '我是父类'
this.skills = ["唱歌", "跳舞", "写代码"]
}
function SubClass() {
this.subName = '我是子类'
}
// 对父类进行继承
SubClass.prototype = new SuperClass()
// 修正constructor
SubClass.prototype.constructor = SubClass
let instance1 = new SubClass()
console.log(instance1.skills); // ["唱歌", "跳舞", "写代码"]
let instance2 = new SubClass()
console.log(instance2.skills); // ["唱歌", "跳舞", "写代码"]
instance1.skills.push("打篮球")
console.log(instance1.skills); // ["唱歌", "跳舞", "写代码", "打篮球"]
console.log(instance2.skills); // ["唱歌", "跳舞", "写代码", "打篮球"]
其二,子类实现的继承是依靠原型对父类实例化实现,所以在创建父类时无法向父类传递参数,即无法实例化父类时对父类构造函数内属性也进行初始化。
二、构造函数式继承
简单讲就是将父类构造函数的内容复制给了子类的构造函数。来看以下代码:
// 构造函数继承
// 声明父类
function SuperClass(name) {
// 值类型共有属性name
this.name = name
// 引用类型共有属性
this.skills = ['java', 'php', 'c++']
}
// 声明父类原型方法
SuperClass.prototype.printSkills = function() {
console.log(this.skills);
}
// 声明子类
function SubClass(name) {
// 继承父类
SuperClass.call(this, name)
}
// 创建第一个子类实例
let instance1 = new SubClass('子类实例一')
let instance2 = new SubClass('子类实例二')
instance1.skills.push('matlab')
console.log(instance1.name);
console.log(instance1.skills); // [ 'java', 'php', 'c++', 'matlab' ]
console.log(instance2.name);
console.log(instance2.skills); // [ 'java', 'php', 'c++' ]
instance1.printSkills() // TypeError
call方法可以改变函数的执行环境,在子类中执行SuperClass.call(this, name)即将子类中的变量name在父类中执行一遍,父类是给this绑定name属性的,因此子类也就继承了父类的共有属性name
[!NOTE]
优缺点和原型链继承恰好相反
优点:
- 父类的引用属性不会被共用
- 子类实例化时可以向父类传递参数
缺点:
父类的方法不能共享复用,违反了代码复用的原则。
到此为止,我们介绍了最简单的原型链式继承和构造函数式继承,发现他们都有很明显且互补的优缺点。要是我们能结合这两种继承方式的优点,就可以得到另外一种继承方式——组合式继承
三、组合式继承
组合继承就是将原型链式继承和构造函数式继承组合起来,吸收两种继承实现方式的优点。**原型链式继承是通过子类的原型prototype对父类实例化来实现,构造函数式继承是通过在子类的构造函数运行环境下执行父类的构造函数来实现。**因此呢,只要稍做变化同时实现这两点即可,请看下面的代码:
// 组合式继承
// 声明父类
function SuperClass(name) {
// 值类型的共有属性
this.name = name
// 引用类型的共有属性
this.skills = ["唱歌", "跳舞", "写代码"]
}
// 父类原型公共方法
SuperClass.prototype.getName = function() {
return this.name
}
// 声明子类
function SubClass(name, age) {
// 构造函数式继承属性name
SuperClass.call(this, name)
// 子类新增其他属性
this.age = age
}
// 类式继承
SubClass.prototype = new SuperClass()
// 修正constructor
SubClass.prototype.constructor = SubClass
// 添加子类自由原型方法
SubClass.prototype.getAge = function() {
return this.age
}
javascript就是这么灵活,我们结合了两种继承方式的优点摒弃了各自的缺点,达到子类既能继承父类的原型属性方法,又能通过向父类传递参数继承父类的共有属性且引用类型互不影响(每个子类各自保留一份)。
当然组合式继承还不是最为完美的继承方式,下一篇我们进一步来讨论更为优雅的继承方式:原型式继承、寄生式继承以及寄生组合式继承。
值此中秋佳节,最后祝大家中秋节快乐!