[JS]带你掌握JS继承

148 阅读6分钟

最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想

前言

上篇文章中我们讲了Javascript的原型链(点击回顾),今天我们就来讲讲利用原型链开发的【继承】内容。

在讲继承之前,必须要先了解什么是【类】。在日常开发中,我们会经常需要创建一些对象用来了描述一个对象的状态。例如我们需要描述一个人我们可能会用以下的一些信息:

var user = {
	id: 1111111,
  username: 'john',
  age: 18,
  registrationTime: '2021-01-01'
};

可是我们不可能对每个人都单独的每次有新的用户需要描述时,都这样重新命名一个新的对象吧。这时我们就会写一个用户【类】,当需要创建新的用户实例时,我们只要从这个类中生产一个实例就行了。

function CreateUser(username,age){
	this.id = getId();
  this.useranme = username;
  this.age = age;
  this.balance = 0this.registrationTime = new Date().getTime();
}
var user = new CreateUser('john',18);

如上述代码,我们一般在定义类的时候,首字母都会用大写开头。通过new 一个新的类,就可以得到一个新的用户实例了。同时一般我们的实例肯定会还有一些方法,此时就可以利用原型,然所有实例都能用到。

CreateUser.prototype.recharge = function(count){
  	this.balance = this.balance + count;
};

原型链结合

接下来我们就可以与原型链结合,来进一步利用类了。假设现在又有一个场景就是在我们新建的用户中,有些是VIP,与普通的用户肯定会有不同的属性。应该怎么处理呢?当我们可以为VIP再写一个类:

function CreateVipUser(username,age){
	this.id = getId();
  this.useranme = username;
  this.age = age;
  this.balance = 0;
  this.registrationTime = new Date().getTime();
  this.isVip = true;
  expirationTime = new Date().getTime() + (1000 * 3600 * 24 * 31);
}
CreateVipUser.prototype.recharge = function(count){
  	this.balance = this.balance + count;
};
var user = new CreateVipUser('john',18);

但我们会发现这个新的类实际上内容并没有比普通用户多多少,可代码却重复了许多。此时利用原型链,我们其实可以把CreateVipUser类设计成是在CreateUser类的基础上再套一层,这样同样的方法只要顺着原型链往上找就行了。

function CreateVipUser(username,age){
  	CreateUser.call(this,username,age);
  	this.isVip = true;
  	this.expirationTime = new Date().getTime() + (1000 * 3600 * 24 * 31);
}
// 把CreateVipUser的原型指向一个CreateUser实例
CreateVipUser.prototype = new CreateUser();
var vipUser = new CreateVipUser({username:'john',age:18});

// vip实例也可以顺着原型链找到recharge方法。
console.log(vipUser.balance); // 0
vipUser.recharge(10);
console.log(vipUser.balance); // 10

继承方式

我们现在已经知道什么是继承了,其实就是顺着原型链一层一层地丰富我们要描述的对象。利用属性继承的方法减少代码量,让我们的开发思路更具体。值得注意的是,与java等天生支持面向对象的语言不同,他们继承的方式是子类继承父类时会把父类的方法复制一份给子类。而Javascript的实现方式,实际上是用原型链来把公共方法指向上一层。所以如果我们修改了子类的公共方式是有可能影响到别的类的。因此我们在指向子类原型时不会直接指向父类的原型,而是拷贝一个新的父类原型或者创建一个父类实例。

接下来我们来看看js的继承方式有哪些:

组合继承

function Parent(name) {
   	//设置属性
 		this... = ...
}
Parent.prototype.method = function () {
   // ...
}

function Child(name, age) {
   // 继承父类属性
   Parent.call(this, name)
   // 设置属性
   this.age = age;
}
// 继承父类方法
Child.prototype = new Parent();
// 设置子类的方法
Child.prototype.method = function () {
   // ...
}

组合继承的方式,充分利用了原型链的特性,并且支持在创建子类时,传递参数。子类原型指向父类的新实例,也避免了对父类原型方法的污染。

组合寄生继承

但其实组合继承不是最好的继承方式,因此我们在创建子类时,需要调用父类的构造函数2次,一次是创建子类时,一次的为子类原型创建实例时。这样的函数调用其实是有成本的,那么有没有方式可以避免这种函数调用呢?业内的方案就是【组合寄生继承】

// 浅拷贝一个对象
function objectCopy(obj) {
  function Fun() { };
  Fun.prototype = obj;
  return new Fun();
}

//  让子类的原型指向拷贝出来的父类原型
function inheritPrototype(child, parent) {
  let prototype = objectCopy(parent.prototype); // 创建对象
  prototype.constructor = child; // 增强对象
  Child.prototype = prototype; // 赋值对象
}

function Parent(name) {
  this.name = name;
  this.friends = ["rose", "lily", "tom"]
}

Parent.prototype.sayName = function () {
  console.log(this.name);
}

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}
// 在子类挂载自己的方法之前实现继承
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function () {
  console.log(this.age);
}

let child1 = new Child("yhd", 23);
child1.sayAge(); // 23
child1.sayName(); // yhd
child1.friends.push("jack");
console.log(child1.friends); // ["rose", "lily", "tom", "jack"]

let child2 = new Child("yl", 22)
child2.sayAge(); // 22
child2.sayName(); // yl
console.log(child2.friends); // ["rose", "lily", "tom"]

可以看到组合寄生继承最大的特点就是完全利用了原型链,子类原型指向的是一个拷贝的父类原型,而不是由执行构造函数后产生的实例。减低了执行函数带来的开销成本。寄生式组合继承可以说是引用类型继承的最佳模式。

// 当然,ES5中新增了一个函数Object.create()可以进一步优化我们的写法
Child.prototype = Object.create(Parent.prototype);

class继承

ES6为我们带来了class的语法,我们可以用更少的代码实现类的创建和继承了。

用class创建类

class其实是个语法糖,创建一个类时大致如下:

class Parent {
    constructor(x) {
        this.x = x;
    }
    getX() {}
}

var parent = new Parent(100);

如果类不需要传参的话,可以简写construct

class Parent {
  	x=100;
    getX() {}
}

var parent = new Parent();

class的继承

class的继承我们需要用到extends关键字。

class Parent {
    constructor(x) {
        this.x = x;
    }
    getX() {}
}

// 在子类定义时,用extends关键字声明这个类继承于Parent
class Child extends Parent {
    constructor(z) {
        // 一但使用extends,并且编写了constructor,必须在constructor函数第一行写上 super()
        //   从原理上类似call继承  super() ==> Parent.call(this),super可以传参给父类
        super(z);
        this.y = 200;
    }
    getY() {}
}

let ch = new Child(100);
console.dir(ch);

class继承与组合寄生继承的区别

class语法糖实际上更组合寄生继承是非常相似的,但也有一些细微的区别:

  • ES5 的继承,先通过用的子类构造函数创建自己的this,然后利用父类的构造函数在自己的this上修改内容。
  • 而ES6 的class继承则刚好相反,当我们用extends的时候,实际上会先创建一个父类的实例。然后在调用子类的构建函数(子类的construct)来通过this修改内容。所以子类construct必须先调用 super 方法(因为子类此时没有自己的this),不然会报错。

总结

今天我们掌握了js类的继承的写法,以及class的用法。同时我们了解了这2种继承方式的区别。希望大家都能有所收获,在工作中可以派上用场。

参考

juejin.cn/post/691421…

juejin.cn/post/696031…