JS 实现继承的几种方式

3,319 阅读3分钟

一.原型链继承

关键核心:让父类的实例作为子类的原型

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student() {
  this.score = 99;
}
Student.prototype = new Person();
Student.prototype.goToSchool = function () {
  console.log('去上学~');
};
let s1 = new Student();
console.log(s1);
console.log(s1.running());

prototype-chain_extend.png

缺点:不能传递参数,且引用值类型的数据会被实例共享。

看下面代码:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = ['Yasuo', 'Zed', 'Yi'];
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student() {
  this.score = 99;
}
Student.prototype = new Person();
Student.prototype.goToSchool = function () {
  console.log('去上学~');
};
let s1 = new Student();
let s2 = new Student();
s1.friends.push('LeeSin');
console.log(s1.friends);
console.log(s2.friends);

prototype-chain-shortage.png

可以看到,明明我只往s1friends中 push 了LeeSin,但是s2friends中也有LeeSin了,这就是原型链继承的缺点之一:引用值类型的数据会被实例共享

二.盗用构造函数继承

关键核心:在子类构造函数中使用 call() 调用父类构造函数

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = ['Yasuo', 'Zed', 'Yi'];
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student(name, age) {
  Person.call(this, name, age);
  this.score = 99;
}
// Student.prototype = new Person();
Student.prototype.goToSchool = function () {
  console.log('去上学~');
};
let s1 = new Student('Cyan', 18);
let s2 = new Student('Csy', 22);
console.log(s1.name);
console.log(s2.name);
s1.friends.push('LeeSin');
console.log(s1.friends);
console.log(s2.friends);
console.log(s1.running());

stealing-constructor_extend.png

优点:解决了原型链继承中不能传参且引用值共享问题。
缺点:由上图可知,这个方式不能调用父类原型上的方法,因为 Student.prototype 和 Person.prototype 根本没有任何关系。

三.组合继承(原型链 + 构造函数)

关键核心:使用 call() 调用父类的属性,使用 new 获取父类原型上的方法

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = ['Yasuo', 'Zed', 'Yi'];
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student(name, age) {
  Person.call(this, name, age);
  this.score = 99;
}
Student.prototype = new Person();
Student.prototype.goToSchool = function () {
  console.log('去上学~');
};
let s1 = new Student('Cyan', 18);
let s2 = new Student('Csy', 22);
console.log(s1.name);
console.log(s2.name);
s1.friends.push('LeeSin');
console.log(s1.friends);
console.log(s2.friends);
console.log(s1.running());

combination_extend.png

优点:解决了不能调用父类原型上的方法的问题。
缺点:多次调用了父类构造函数。

实际上,父类构造函数中的this.namethis.age在子类中是取不到的。因为通过子类 new 出来的对象在访问属性或者方法时,会先看 new 出自己的类上面有没有这个属性和方法,没有才去原型上找,原型上没有再去父类找,父类找不到再去父类的原型上找。显然,你都已经调用 call 了,那么父类(Person)中的属性和方法子类(Student)中必然也有。

四.寄生式组合继承

关键核心:使用 Object.create() 来解决多次调用父类构造函数问题

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = ['Yasuo', 'Zed', 'Yi'];
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student(name, age) {
  Person.call(this, name, age);
  this.score = 99;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.goToSchool = function () {
  console.log('去上学~');
};
let s1 = new Student('Cyan', 18);
let s2 = new Student('Csy', 22);
console.log(s1.name);
console.log(s2.name);
s1.friends.push('LeeSin');
console.log(s1.friends);
console.log(s2.friends);
console.log(s1.running());

parasitism_extend.png prototype-chain-extent

Student.prototype = Object.create(Person.prototype)这段代码的意思是先用 Object.create() 创建一个新对象,然后让这个新对象的__proto__ 指向Person.prototype,最后再让Student.prototype.__proto__指向这个新对象。

注意!不能用Student.prototype = Person.prototype,因为如果这时一个 Teacher 子类也想继承 Person 类并且如果 Student 或者 Teacher 修改了他们对应的原型上的方法的话,另外一个类的实例也会受到影响,如下面代码:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student(name, age) {
  Person.call(this, name, age);
  this.score = 99;
}
function Teacher(name, age) {
  Person.call(this, name, age);
  this.salary = 9999;
}
Student.prototype = Person.prototype;
Teacher.prototype = Person.prototype;

let s1 = new Student('Cyan', 18);
let t1 = new Teacher('Csy', 22);

Student.prototype.running = function () {
  console.log('学生在跑步~');
};
console.log(s1.running());
console.log(t1.running());

prototype-chain-shortage.png

可以看到当我修改了 Student.prototype 上的 running 方法后,t1 实例调用 running 时结果也改变了,这显然不是我们想要的。 使用 Object.create() 就不会发生这样的情况了,看下面代码:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student(name, age) {
  Person.call(this, name, age);
  this.score = 99;
}
function Teacher(name, age) {
  Person.call(this, name, age);
  this.salary = 9999;
}
Student.prototype = Object.create(Person.prototype);
Teacher.prototype = Object.create(Person.prototype);

let s1 = new Student('Cyan', 18);
let t1 = new Teacher('Csy', 22);

Student.prototype.running = function () {
  console.log('学生在跑步~');
};
console.log(s1.running());
console.log(t1.running());

object-create.png

当修改Student.prototype上的方法时不会对Teacher.prototype造成影响。实际上,他们两个指向的不是同一个对象。

console.log(Student.prototype === Teacher.prototype); // false

如果是下面这种情况:

Student.prototype = Person.prototype;
Teacher.prototype = Person.prototype;
console.log(Student.prototype === Teacher.prototype); // true

因为他们两个都指向Person.prototype,所以当其中一个对象改变时,另一个也会跟着改变。

好的,以上就是关于 JS 实现继承的几种方法!