最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想
前言
上篇文章中我们讲了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 = 0;
this.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种继承方式的区别。希望大家都能有所收获,在工作中可以派上用场。