javaScript中的类和继承

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

特点:

  1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  2. 父类新增原型方法/原型属性,子类都能访问到
  3. 简单,易于实现

缺点:

  1. 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
  2. 无法实现多继承
  3. 来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)(详细请看附录代码: 示例1
  4. 创建子类实例时,无法向父类构造函数传参

推荐指数:★★(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. 解决了1中,子类实例共享父类引用属性的问题
  2. 创建子类实例时,可以向父类传递参数
  3. 可以实现多继承(call多个父类对象)

缺点:

  1. 实例并不是父类的实例,只是子类的实例
  2. 只能继承父类的实例属性和方法,不能继承原型属性/方法
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

推荐指数:★★(缺点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

特点:

  1. 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果

缺点:

  1. 实例是父类的实例,不是子类的实例
  2. 不支持多继承

推荐指数:★★

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

特点:

  1. 支持多继承

缺点:

  1. 效率较低,内存占用高(因为要拷贝父类的属性)
  2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用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

特点:

  1. 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
  2. 既是子类的实例,也是父类的实例
  3. 不存在引用属性共享问题
  4. 可传参
  5. 函数可复用

缺点:

  1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

推荐指数:★★★★(仅仅多消耗了一点内存)

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; // 需要修复下构造函数

特点:

  1. 堪称完美

缺点:

  1. 实现较为复杂

推荐指数:★★★★(实现复杂,扣掉一颗星)

附录代码:

示例一:

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 中的重大升级之一的优势在哪里:

  1. class 写法更加简洁、含义更加明确、代码结构更加清晰。
  2. class 尽管也是函数,却无法直接调用(不存在防御性代码了)。
  3. class 不存在变量提升。
  4. class 为污染 window 等全局变量(这点很赞啊)。
  5. 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在静态方法中指向了父类,在普通函数中指向了父类的原型。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值