JavaScript是支持面向对象编程的,面向对象编程的三大特性——封装、继承、多态,在JavaScript中都能有所体现。在从前的JavaScript的世界中,实现继承并不是一件轻松的事情,必须理解关于原型链的相关概念和知识,才能较好地实现继承。
ES6推出后,我们可以通过class关键字按照经典的继承语法来实现继承,但其底层仍然是原型继承。可见class是一个语法糖。
class初体验
class是ES6推出的关键字,运用class,我们可以较为优雅地实现继承,语法是经典的extends实现继承关系,一改以往痛苦的原型继承。但是别忘了,class是一个语法糖,底层仍然是基于原型。
class Employee {
// 此处为构造函数,当实例化一个对象时
// 会通过调用此函数,来对实例化对象进行初始化
constructor(name, gender, age) {
this.name = name;
this.gender = gender;
this.age = age;
}
punchClock() {
console.log(`Good morning ${this.name}`);
}
}
我们可以通过new关键字,创建一个Employee的实例,代码如下:
class Employee {
constructor(name, gender, age) {
this.name = name;
this.gender = gender;
this.age = age;
}
punchClock() {
console.log(`Good morning ${this.name}`);
}
}
let hans = new Employee("hans", "male", 22);
hans.name;//"hans"
hans.gender;//"male"
hans.age;//"22"
hans.punchClock();//Good morning hans
上面的代码,我们创建了一个Employee类,构造函数将对实例化对象进行相关属性的初始化;而punchClock是Employee原型上的方法,凡是Employee的实例化对象,均可进行调用。
如果不使用class来实现同样的功能,则运用到原型的概念,代码如下:
function Employee(name, gender, age) {
this.name = name;
this.gender = gender;
this.age = age;
}
Employee.prototype.punchClock = function () {
console.log(`Good morning ${this.name}`);
}
原型是为何物?在JS的世界中,当我们创建一个命名以大写开头的方法,就会默认是构造方法。若我们通过new关键字来创建一个实例化对象时,会做四件事情,分别是:
- 创建一个新的对象
- this指向新的对象,将构造函数的作用域给新对象
- 执行构造函数中的代码,初始化属性
- 返回该新对象
在以上四步结束之后,构造方法和实例化对象关系如下图所示:
在浏览器控制台进行打印,如下:
ES6之前的继承实现
在class关键字出现之前,JS的继承实现并不轻松,主要通过原型链模拟继承。常见的继承方法如下:
- 原型继承
- 原型链继承
- 构造函数继承
- 组合继承
- 寄生式继承
- 寄生组合式继承
每一种继承方法各有特点,都是巧妙的使用原型知识实现继承,但都有一定的限制和缺陷。本文我们以原型继承为例进行讲解。
原型对象的作用就是为类的原型添加公有方法,但类不能直接访问这些属性和方法,必须通过prototype来访问。而我们实例化一个父类的时候,新创建的对象赋值了父类的原型对象上的属性和方法,并且这个实例化对象可以直接访问到父类原型对象上的属性与方法。同时,如果我们将这个新创建的对象赋值给子类的原型,那么子类的原型就可以访问到父类的原型属性和方法
代码如下:
function Employee(name, gender, age) {
this.name = name;
this.gender = gender;
this.age = age;
this.task = ["manage", "organize"];
}
//为父类添加公有方法
Employee.prototype.punchClock = function () {
console.log(`Good morning`);
}
//声明子类
function Manager(){}
//继承父类
Manager.prototype = new Employee();
//实例化子类
let manager = new Manager();
console.log(Manager.prototype instanceof Employee);//true
但是这种原型继承有致命缺点,由于子类通过原型对父类实例化,继承了父类。所以当实例化对象更改了父类中的公有引用类型属性,会影响到其他的子类。如下所示:
function Employee(name, gender, age) {
this.name = name;
this.gender = gender;
this.age = age;
this.task = ["manage", "organize"];
}
//声明子类
function Manager(){}
//继承父类
Manager.prototype = new Employee();
//实例化子类
let manager1 = new Manager();
let manager2 = new Manager();
console.log(manager1.task);//["manage", "organize"]
console.log(manager2.task);//["manage", "organize"]
manager1.task.push("sleep");
console.log(manager1.task);//["manage", "organize", "sleep"]
console.log(manager2.task);//["manage", "organize", "sleep"]
class实现继承
class关键字实现继承则要方便得多,
class Employee {
constructor(name) {
this.name = name;
}
punchClock() {
console.log(`Good morning ${this.name}`);
}
}
class Manager extends Employee {
constructor(name, age) {
// super调用基类构造函数
super(name);
this.age = age;
}
manageTeam() {
console.log(`${this.name} manage a team`);
}
}
let manager = new Manager("Hans", 22);
manager.manageTeam();// Hans manage a team
manager.punchClock();// Good morning Hans
在以上的代码中,使用extends就可以轻松实现继承关系,manager虽然是Manager的实例化对象,但同样可以使用父类的punchClock打卡方法。经理既是职位,又是员工;既要履行经理的基本职责,作为员工又要每日打卡。
class关键字的引入,让JavaScript的继承更加优雅,更方便地实现模拟类,但是底层仍然是基于原型实现的。