ES5之前的继承
既然要实现继承,那么首先我们得有一个父类,代码如下:
// 定义一个动物类
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
1、原型链继承
核心: 将父类的实例作为子类的原型
function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
特点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
- 父类新增原型方法/原型属性,子类都能访问到
- 简单,易于实现
缺点:
- 要想为子类新增属性和方法,必须要在
new Animal()
这样的语句之后执行,不能放到构造器中 - 无法实现多继承
- 来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)(详细请看附录代码: 示例1)
- 创建子类实例时,无法向父类构造函数传参
推荐指数:★★(3、4两大致命缺陷)
2、构造继承
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
特点:
- 解决了1中,子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
推荐指数:★★(缺点3)
3、实例继承
核心:为父类实例添加新特性,作为子类实例返回
function Cat(name){
var instance = new Animal();
instance.name = name || 'Tom';
return instance;
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false
特点:
- 不限制调用方式,不管是
new 子类()
还是子类()
,返回的对象具有相同的效果
缺点:
- 实例是父类的实例,不是子类的实例
- 不支持多继承
推荐指数:★★
4、拷贝继承
function Cat(name){
var animal = new Animal();
for(var p in animal){
Cat.prototype[p] = animal[p];
}
//如下实现修改了原型对象,会导致单个实例修改name,会影响所有实例的name值
// Cat.prototype.name = name || 'Tom'; 错误的语句,下一句为正确的实现
this.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
特点:
- 支持多继承
缺点:
- 效率较低,内存占用高(因为要拷贝父类的属性)
- 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
推荐指数:★(缺点1)
5、组合继承
核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
特点:
- 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
- 既是子类的实例,也是父类的实例
- 不存在引用属性共享问题
- 可传参
- 函数可复用
缺点:
- 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
推荐指数:★★★★(仅仅多消耗了一点内存)
6、寄生组合继承
核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
(function(){
// 创建一个没有实例方法的类
var Super = function(){};
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();
})();
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
Cat.prototype.constructor = Cat; // 需要修复下构造函数
特点:
- 堪称完美
缺点:
- 实现较为复杂
推荐指数:★★★★(实现复杂,扣掉一颗星)
附录代码:
示例一:
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
//实例引用属性
this.features = [];
}
function Cat(name){
}
Cat.prototype = new Animal();
var tom = new Cat('Tom');
var kissy = new Cat('Kissy');
console.log(tom.name); // "Animal"
console.log(kissy.name); // "Animal"
console.log(tom.features); // []
console.log(kissy.features); // []
tom.name = 'Tom-New Name';
tom.features.push('eat');
//针对父类实例值类型成员的更改,不影响
console.log(tom.name); // "Tom-New Name"
console.log(kissy.name); // "Animal"
//针对父类实例引用类型成员的更改,会通过影响其他子类实例
console.log(tom.features); // ['eat']
console.log(kissy.features); // ['eat']
原因分析:
关键点:属性查找过程
执行tom.features.push,首先找tom对象的实例属性(找不到),
那么去原型对象中找,也就是Animal的实例。发现有,那么就直接在这个对象的
features属性中插入值。
在console.log(kissy.features); 的时候。同上,kissy实例上没有,那么去原型上找。
刚好原型上有,就直接返回,但是注意,这个原型对象中features属性值已经变化了。
ES6中的Class类和继承
EcmaScript 2015 (又称ES6)通过一些新的关键字,使类成为了JS中一个新的一等公民。但是目前为止,这些关于类的新关键字仅仅是建立在旧的原型系统上的语法糖,所以它们并没有带来任何的新特性。不过,它使代码的可读性变得更高,并且为今后版本里更多面向对象的新特性打下了基础。
class 作为 ES6 中的重大升级之一的优势在哪里:
- class 写法更加简洁、含义更加明确、代码结构更加清晰。
- class 尽管也是函数,却无法直接调用(不存在防御性代码了)。
- class 不存在变量提升。
- class 为污染 window 等全局变量(这点很赞啊)。
- class 函数体中的代码始终以严格模式执行(新手写的代码可靠性也必须上来了)
class Parent {
constructor (name, age) {
this.name = name
this.age = age
}
getUserInfo() {
console.log(this.name, this.age)
}
}
class Child extends Parent {
constructor(name, age) {
super(name, age)
this.hobby = ['running', 'reading']
}
}
const child = new Child('fuhangyy', 27)
child.getUserInfo()
// fuhangyy 27
console.log(child.hobby)
// ['running', 'reading']
console.log(child instanceof Child)
// true
console.log(child instanceof Parent)
// true
我们可以看到在子类的constructor方法调用了一个super方法,它在这里主要表示父类的构造函数,用来新建父类的this对象。
并且在子类的constructor方法中必须调用super方法,否则在新建子类的实例时是会报错的。因为子类中其实是没有this对象的,而是需要继承父类的this对象,然后对其加工,所以如果不调用super方法,也就得不得父类的this对象,子类也就没法对其进行加工,从而得不到自己的this对象了。
还可以看出来实例对象child同时是Child和Parent来个类的实例,这与ES5的继承行为是一致的。
class Parent {}
class Child extends Parent {
constructor() {}
}
const child = new Child()
上面的示例代码中,用于在子类Child中没有调用super方法,导致在创建child示例时报错。
ES5中的继承的本质其实是先创建子类的实例对象this,然后再将父类的方法添加到this上面去(Parent.call(this))。但是Class的继承机制完全不同,它是先创建父类的实例对象的this(调用super方法),然后再用子类的构造函数修改this。
我们都知道在使用Class定义类的时候,如果没有添加constructor方法时,都会默认添加的,在继承时也不例外:
class Child extends Parent {}
// 等价于
// 上面的例子中调用了consturctor方法,但是没有调用super方法,会报错的
class Child extends Parent {
constructor (...arguments) {
super(...arguments)
}
}
还有一点需要注意的是,在子类中只有调用了super方法,才可以使用this对象,原因上面已经说过了。
class Child extends Parent {
constructor(name, age) {
this.hobby = ['running', 'reading'] // 这里使用this时是会报错
super(name, age)
this.hobby = ['running', 'reading']
}
}
这里补充一个小知识,我们可以通过Object.getPrototypeOf()方法从子类上获取到父类。
Object.getPrototypeOf(Child) === Parent
// true
下面来一起看看super关键字,它既可以当函数来调用,也可以当做对象来使用,这两种情况下,他们的用法是完全不同的。
当super被当做函数来调用时,它代表的父类的构造函数,用来新建父类的this对象,提供给子类使用。所以在子类的constructor方法中必须要调用一次super方法,而且是只能在子类的constructor方法中调用,在其他地方调用就会导致报错。
super虽然代表了父类Parent的构造函数,但是返回的是子类Child的实例,即super内部的this指向的是子类Child,因此super()在这里相当于Parent.prototype.constructor.call(this)。
当super作为对象在普通方法中使用时指向父类的原型对象,在静态方法中使用时指向父类。
class Parent {
constructor (name) {
this.name = name
}
getName () {
console.log(this.name)
}
}
class Child extends Parent {
constructor (name) {
super(name)
super.getName()
// fuhangyy
console.log(super.name)
// undefined
}
}
const child = new Child('fuhangyy')
由于class类的其他方法默认是添加在类的原型上面的,所以在调用super.getName()时,调用的父类原型对象上面的方法,所以可以打印出数据,但是name属性是定义在父类的实例中的,所以值为undefined。
接下来,我们看看下面这个例子:
class Parent {
constructor () {
this.name = 'fuhangyy'
}
getName () {
console.log(this.name)
}
}
class Child extends Parent {
constructor () {
super()
this.name = 'RubyOnly'
}
getUserInfo () {
super.getName()
}
}
const child = new Child()
child.getUserInfo()
// RubyOnly
child.getName()
// RubyOnly
调用child.getName(),打印出RubyOnly很好理解,child继承了Parent中的getName方法,但是我们调用child.getUserInfo()打印出来的也是RubyOnly,super.getName()虽然调用的是Parent.prototype.getName(),但其实 Parent.prototype.getName()会绑定Child的this,所以最后打印出来的是RubyOnly,因此我们可以总结出,通过super调用父类方法时,会绑定子类的this。
既然通不过super调用父类的方法是会将this绑定到子类上,那么通过super修改父类的某个属性时,被修改的属性就变成了子类的实例属性了。
class Parent {
constructor () {
this.name = 'fuhangyy'
}
}
class Child extends Parent {
constructor () {
super()
super.name = 'RubyOnly'
console.log(super.name)
// undefined 在普通函数中super调用的是父类原型对象上面的属性和方法
console.log(this.name)
// RubyOnly
}
}
const child = new Child()
但是如果super在静态方法中调用,此时super指向的是父类。
class Parent {
static getName (name) {
console.log(`static ${ name }`)
}
getName(name) {
console.log(`prototype ${ name }`)
}
}
class Child extends Parent {
static getName (name) {
super.getName(name)
}
getName (name) {
super.getName(name)
}
}
Child.getName('fuhangyy')
// static fuhangyy
const child = new Child()
child.getName('fuhangyy')
// prototype fuhangyy
我们可以看出来,super在静态方法中指向了父类,在普通函数中指向了父类的原型。