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指向新的对象,将构造函数的作用域给新对象
  • 执行构造函数中的代码,初始化属性
  • 返回该新对象

在以上四步结束之后,构造方法和实例化对象关系如下图所示:

es 关键词搜索 es 关键字_实例化


在浏览器控制台进行打印,如下:

es 关键词搜索 es 关键字_父类_02

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的继承更加优雅,更方便地实现模拟类,但是底层仍然是基于原型实现的。

es 关键词搜索 es 关键字_父类_03