ES5中,社区通过各种方式来实现类似于类(class-like)的行为。但各种方式都有自己的问题和妥协。所以实现继承的代码也显得非常冗长和混乱
原型链继承:不能复用方法,引用类型会被共享,子类型在实例化时不能给父类型的构造函数传参。
盗用构造函数继承:解决原型包含引用值导致共享问题,以及可以向父类构造函数传递参数的问题。但还有方法不能复用的问题。
组合继承:组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。但却有效率问题。即父类够造函数会执行两次。
寄生式组合继承:解决了组合继承重复调用两次父类构造函数,以及子类原型上不必要的重复属性。寄生式组合继承可以算是引用类型继承的最佳模式。
为解决这些问题,ECMAScript 6 新引入的 class 关键字具有正式定义类的能力。class 表面上看起来像是支持正式的面向对象编程,但实际上还是基于原型和够造函数实现的。
类构造函数
当我们调用new时,调用的就是constructor。构造函数的定义不是必需的,不定义构造函 数相当于将构造函数定义为空函数。
类构造函数与普通够造函数创建对象的过程是一样的,主要区别是,调用类构造函数必须使用 new 操作符。而普通构造函数如果 不使用 new 调用,那么就会以全局的 this(通常是 window)作为内部对象。调用类构造函数时如果忘了使用 new 则会抛出错误:
function Person() {}
class Animal {}
// 把 window 作为 this 来构建实例
let p = Person();
let a = Animal();
// TypeError: class constructor Animal cannot be invoked without 'new'
类是一个特殊函数
ECMAScript 中没有正式的类这个类型,,ECMAScript 类就是一种特殊函数。声明一个类之后,通过 typeof 操作符检测类标识符,表明它是一个函数:
class Person {}
console.log(Person); // class Person {}
console.log(typeof Person); // function 是一个函数
类本身具有与普通够造函数一样的行为
//class
class Person{}
console.log(Person.prototype); // { constructor: f() }
console.log(Person === Person.prototype.constructor); // true
let p = new Person();
console.log(p instanceof Person); // true
//普通构造函数
function Student(){}
console.log(Student.prototype); // { constructor: f() }
console.log(Student === Student.prototype.constructor); // true
let student = new Student()
console.log(student instanceof Person);//true
实例成员、原型成员和类成员
类的语法可以非常方便地定义应该存在于实例上的成员、应该存在于原型上的成员,以及应该存在于类本身的成员。
实例成员
new调用类标识符时,会调用constructor函数,这个constructor函数返回的对象就是每个实例对象,所以在constructor中定义的属性和方法都是实例成员
class Person {
constructor() {
// 使用包装对象为的是在下面测试两个对象的相等性
this.name = new String('Jack');
this.sayName = () => console.log(this.name);
this.nicknames = ['Jake', 'J-Dog']
}
}
let p1 = new Person(), p2 = new Person();
p1.sayName(); // Jack
p2.sayName(); // Jack
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nicknames === p2.nicknames); // false
原型成员
为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('实例成员');
}
// 在类块中定义的所有内容都会定义在类的原型上
locate() {
console.log('原型成员');
}
}
let p = new Person();
p.locate(); // 实例成员
Person.prototype.locate(); // 原型成员
类成员
可以在类上定义静态方法。这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。静态类成员在类定义中使用 static 关键字作为前缀。在静态成员中,this 引用类自身。其他所 有约定跟原型成员一样:
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance', this);
}
// 定义在类的原型对象上
locate() {
console.log('prototype', this);
}
// 定义在类本身上
static locate() {
console.log('class', this);
}
}
let p = new Person();
p.locate(); // instance, Person {}
Person.prototype.locate(); // prototype, {constructor: ... }
Person.locate(); // class, class Person {...}
数据成员
类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加:
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`);
}
}
// 在类上定义数据成员
Person.greeting = 'My name is';
// 在原型上定义数据成员
Person.prototype.name = 'Jake';
let p = new Person();
p.sayName(); // My name is Jake
ES6的继承
ES6 类支持单继承。使用class定义的类 可以继承任何拥有[[Construct]]和原型的对象。意味着,class可以继承一个普通的构造函数(保持向后后兼容)
class Vehicle {}
// 继承类
class Bus extends Vehicle {}
function Person() {}
// 继承普通构造函数
class Engineer extends Person {}
const chentao = new Engineer()
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true
派生类都会通过原型链访问到类和原型上定义的方法:
class Vehicle {
//原型上的方法
identifyPrototype(id) {
console.log(id, this);
}
//类上的方法
static identifyClass(id) {
console.log(id, this);
}
}
class Bus extends Vehicle {}
const yutongkeche = new Bus()
const benchi = new Vehicle()
//通过实例调用的方法是原型上的方法
yutongkeche.identifyPrototype()// Bus {}
benchi.identifyPrototype()// Vehicle {}
//通过类调用的方法是类上的方法(静态方法)
Vehicle.identifyClass()//class Vehicle {...}
Bus.identifyClass()//class Bus extends Vehicle {}