JS的继承方式

196 阅读5分钟

1. 原型链继承

子类型的原型为父类型的一个实例对象。

function Person(name, age) {
  //这里的是实例(私有)属性/方法
  this.name = name;
  this.age = age;
  this.play = [1, 2, 3];
  this.setName = function () {};
}
Person.prototype.setAge = function () {};
//这里的是原型(公有)属性/方法
function Student(price) {
  this.price = price;
  this.setScore = function () {};
}
//子类的原型指向父类的实例,这样就可以顺着原型链共享父类的方法了。并且为子类添加原型方法的时候,不会影响父类。
Student.prototype = new Person();
var s1 = new Student(1500);
var s2 = new Student(1400);
console.log(s1, s2);

子类的实例就可以通过__proto__访问到Student.prototype也就是Person的实例,这样就可以访问到父类的实例属性和方法;然后再通过Person实例的__proto__指向父类的prototype就可以获得到父类原型上的方法。这样就做到了将父类的实例 、原型属性和方法都当作子类的原型属性和方法。

特点

  • 父类新增原型属性 /方法,子类都能访问到
  • 简单,易于实现

缺点

  • 创建子类实例时,无法向父类构造函数传参
  • 继承后的所有属性都在原型上,被所有子实例共享。(如果父类的实例属性中有引用类型的属性,那么它被子类继承的时候会作为原型属性。这样子类1操作这个属性的时候,就会影响到子类2)
  • 要想为子类中添加新的方法,一定要在替换原型的语句(Student.prototype = new Person())之后
  • 无法实现多继承

2. 借用构造函数继承

在子类型构造函数中用call()调用父类型构造函数

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.setName = function () {};
}
Person.prototype.setAge = function () {};
function Student(name, age, price) {
  Person.call(this, name, age);
  this.price = price;
}
var s1 = new Student('Tom', 20, 1500);

特点:

  • 创建子类实例时,可以向父类的构造函数传递参数
  • 子类实例不再共享父类私有属性中的引用属性,因为这些属性不在子类的原型上  
  • 可以实现多继承(call多个父类对象)

缺点:

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

3. 原型链+借用构造函数组合继承

调用父类构造函数,继承父类的实例属性并保留了传参的优点;然后将父类实例作为子类原型,继承了原型属性。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.setName = function () {};
}
Person.prototype.setAge = function () {};
function Student(name, age, price) {
  Person.call(this, name, age); //父类实例属性以这次为准
  this.price = price;
  this.setScore = function () {};
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
Student.prototype.sayHellop = function () {};
var s1 = new Student('Tom', 20, 1500);
console.log(s1);
console.log(s1.constructor);

融合了原型链继承和构造函数继承的优点。

但无论什么情况下都会调用两次构造函数。一次在创建子类型原型的时候,另一次实在子类型构造函数的内部。父类型的实例属性被重写了。

优点:

  • 可以继承实例属性/方法,也可以继承原型属性/方法
  • 可传参
  • 不存在引用属性共享问题
  • 函数可复用

缺点:

  • 调用了两次父类构造函数,生成了两份实例

4. 组合继承优化

改变了原型链继承中,替换原型时创建对象的方法 。使用Student.prototype = Object.create(Person.prototype),来继承父原型对象的属性/方法。

构造函数继承不变。

实例和原型属性/方法都可以继承,可传参,还避免了调用两次父类构造函数。

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.setAge = function () {};
function Student(name, age, price) {
  Person.call(this, name, age);
  this.price = price;
  this.setScore = function () {};
}
Student.prototype = Object.create(Person.prototype); //核心代码
Student.prototype.constructor = Student; //核心代码
var s1 = new Student('Tom', 20, 1500);

5. ES6中class的继承

ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法。

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.call(this));ES6的继承,实质是先将父类实例对象的属性和方法加到this上面,然后再用子类的构造函数修改this。

class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。

//基于ES6 class的继承
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  showName() {
    console.log('父类方法');
    console.log(this.name);
  }
}
class Student extends Person {
  constructor(name, age, salary) {
    super(name, age); //通过super()调用父类的构造方法,继承实例属性
    this.salary = salary;
  }
  personShowName() {
    return super.showName(); //通过super引用父类原型对象上的方法,继承原型方法
  }
  studentShowName() {
    console.log('子类方法');
    console.log(this.name, this.salary);
  }
}
let s1 = new Student('baba', 38, 1000);
s1.personShowName();
s1.studentShowName();

可以使用super继承父类的实例方法,原型方法,静态方法

super关键字

super既可以当作函数使用,也可以当作对象使用。

1. super当作函数调用时,代表父类的构造函数。子类的构造函数中,必须使用super()调用一次父类的构造函数。实现实例属性/方法继承。

2. super当作对象时,在普通方法中指向父类的原型对象;在静态方法中,指向父类 。

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}
class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  printB() {
    super.print(); //super指向父类的原型对象
  }
}
let b = new B();
b.printB(); //2

class A {
  constructor() {
    this.x = 1;
  }
  static print() {
    console.log(this.x);
  }
}
class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static printB() {
    super.print(); //super指向父类
  }
}
B.x = 3;
B.printB(); //3

类的__proto__属性和prototype属性

  1. 子类的__proto__属性,表示构造函数的继承,总是指向父类 
  2. 子类的prototype属性的__proto__的属性,表示方法的继承,总是指向父类的prototype属性

代码:

class A{
}
class B extends A{
}
B.__proto__ === A //true
B.prototype.__proto__ === A.prototype //true

实例的__proto__属性

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。

class A {}
class B extends A {}
let a = new A();
let b = new B();
b.__proto__.__proto__ === a.__proto__; //true