相信很多小伙伴在面试时都被面试官问过:给你几分钟,你跟我说下JS的继承。JS的继承方式可以说是五花八门,好多小伙伴都不能系统地将其讲解出来。所以今天我们就来唠一唠这个JS的继承方式。
ES5的继承
原型链继承
我们都知道如果访问一个对象本身的属性,它首先会在自身查找是否有这个属性,如果没有就到它的原型链上去寻找。所以第一种也是最简单的一种方式,可以直接将子类的原型赋值为new一个父类的实例。这样子类就可以继承或重写父类构造函数上的和原型链上的属性和方法了。
function Father(a, b) {
this.a = a;
this.b = b;
this.fn = function() {
console.log('father');
}
}
function Son(c) {
this.c = c;
}
Son.prototype = new Father('a', 'b');
const s = new Son('c');
console.log(s.a);
console.log(s.b);
console.log(s.c);
s.fn();
但是这种继承并不完美!!!
1、子类实例化的时候,也就是new Son()的时候,如果不想重写父类的构造函数,我们根本向父类构造函数就无法传参。想要传参,就必须重写父类的所有属性。
所以为了解决这个问题,就出现了第二种继承方式:构造函数继承。
构造函数继承
这种方式实现也很简单,就是在子类的构造函数内部,通过call改变this指向,借用父类的构造函数。将this指向父类的构造函数,这样就可以继承父类构造函数上的属性和方法。
function Father(a, b) {
this.a = a;
this.b = b;
this.fn = function() {
console.log('father');
}
}
function Son(a, b, c) {
Father.call(this, a, b);
this.c = c;
}
const s = new Son('a', 'b', 'c');
console.log(s.a);
console.log(s.b);
console.log(s.c);
s.fn();
但是这种方法还不是完美的,它无法继承父类原型上的方法!!!
所以这时候,出现了第三种实现继承的方式,组合继承。
组合继承
这种方式其实就是原型链继承和构造函数继承的综合使用,通过改变子类构造函数的this指向父类构造函数,继承父类构造函数内的方法和属性。在通过在原型上new一个父类的实例,继承父类原型链上的属性和方法。
function Father(a, b) {
this.a = a;
this.b = b;
this.fn = function() {
console.log('father');
}
}
Father.prototype.protoFn = function() {
console.log('father prototype fn');
}
function Son(a, b, c) {
Father.call(this, a, b);
this.c = c;
}
Son.prototype = new Father('a1', 'b1');
const s = new Son('a', 'b', 'c');
这种方法子类既可以继承父类构造函数的属性和方法,也能继承父类原型的属性和方法。但是还是有缺点。。。
在实例化的过程中,它调用将父类进行了两次实例化,造成了子类的原型中多了很多不必要的属性。子类的构造函数和原型上,都存在着父类的构造函数上的属性和方法。
那么有没有一种方法,只取父类的构造函数和父类的原型进行继承呢?答案是有的,寄生继承。
寄生继承
现在继承父类构造函数上的属性和方法的方式不变,还是通过改变this指向
子类原型的处理方式不再是new一个父类的实例了,而是通过ES6的Object.create()方法,直接取父类构造函数的原型作为子类构造函数的原型。这样就避免了那些不必要的属性和方法。
function Father(a, b) {
this.a = a;
this.b = b;
this.fn = function() {
console.log('father');
}
}
Father.prototype.protoFn = function() {
console.log('father prototype fn');
}
function Son(a, b, c) {
Father.call(this, a, b);
}
const fatherProtoType = Object.create(Father.prototype);
Son.prototype = fatherProtoType;
const s = new Son('a', 'b', 'c');
ES6的继承
ES6新增的class关键字,可以轻松地使用class来创建一个类,然后使用extends关键字来进行继承。子类实例化想要向父类构造函数传参时,只需要时super关键字即可。
class Father {
constructor(a, b) {
this.a = a;
this.b = b;
this.fn = function() {
console.log('father');
}
}
}
class Son extends Father {
constructor(a, b, c) {
super(a, b);
this.c = c;
}
}
const son = new Son('a', 'b', 'c');
这种方式简直清晰明了,赏心悦目。
但是子类的构造函数内部为什么要使用super关键字呢?还必须在第一句写。不用行不行呢?答案是不行!
我们都知道在使用new进行实例化的时候,constructor内部的this肯定是指向实例化对象。在new Son的时候 Son constructor的this肯定指向son这个对象,然后Son要继承Father,需要调用Father的constructor,那这时father的constructor指向哪呢?是不是不清楚,这不就乱套了吗?所以需要使用super来调用父类的构造函数,来明确Father中的this指向。
extends
那extends又是如何实现继承的呢?这里我查看了TypeScript编译后的ES5代码。
var __extends = (this && this.__extends) || (function(Son, Father) {
var extendsStatic = extendsStatic1 || extendsStatic2 || extendsStatic3;
return function(Son, Father) {
if (typeof Father !== 'function' && Father !== null) throw Error('error');
extendsStatic(Son, Father);
function __() { this.constructor = Father };
Son.prototype = Father === null ? Object.create(Father) : (__.prototype = Father.prototype, new __());
}
})();
var extendsStatic1 = Object.setPrototypeOf;
var extendsStatic2 = ({ __proto__: [] }) instanceof Array && function(Son, Father) {
Son.__proto__ = Father;
}
var extendsStatic3 = function(Son, Father) {
for (var p in Father) {
if (Object.prototype.hasOwnProperty.call(Father, p)) {
Son[p] = Father[p];
}
}
}
var Son = /** @class */ (function(_super) {
__extends(Son, _super); // extends函数
function Son(a, b, c) {
var _this = _super.call(this, a, b) || this; // 调用父级构造函数
_this.c = c;
return _this;
}
return Son;
}(Father));
其实主要看两处地方,__extends方法和_super.call(this,a,b);
__extends方法的作用是:内部使用了SetPrototypeOf方法,将Son.prototype的__proto__指向Father。
_super.call(this,a,b)这句代码的作用就是直接在调用Father构造函数。
可见extends的本质就是组合继承的语法糖。