前言
ES5 及早期版本中没有类的概念,接近的办法就是创建自定义类型:首先创建构造函数,然后定义另一方法并赋值给构造函数的原型。ES6 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。
类声明
定义一个类的一种方法:使用类声明,使用 class 关键字。
class MyClass {
// 类的构造函数
constructor (name) {
this.name = name;
}
getName () {
return this.name;
}
}
let myClass = new MyClass("111");
console.log(myClass.getName());
// 自定义类型,等价于上面 MyClass
let MyClass = (function () {
"use strict";
const MyClass = function (name) {
if (typeof new.target === "undefined") {
throw new Error("请使用new调用构造函数");
}
this.name = name;
};
Object.defineProperty(MyClass.prototype, "getName", {
value: function () {
if (typeof new.target !== "undefined") {
throw new Error("不可以通过new来调用");
}
return this.name;
},
writable: true,
enumerable: false,
configurable: true
});
return MyClass;
})();
上面代码一种通过class声明的,一种通过 ES5 语法实现等价类的特性。
class 类语法要注意几点:
- 类声明无法提升,存在临死死区;
- 类声明代码会自动运行在严格模式之下,无法改变;
- 必须使用 new 方式调用类的构造函数;
- 在类中修改类名会报错。
类表达式
类和函数都存在两种形式:声明形式和表达形式。类声明形式上面已经相关的代码列举了,下面介绍类表达式:
// 第一种:匿名类表达
let MyClass = class {
constructor () {
// 代码
},
// 其他属性、方法
}
// 第二种:命名类表达
let MyClass1 = class MyClass2 {
constructor () {
// 代码
},
// 其他属性、方法
}
console.log(typeof MyClass1); // function
console.log(typeof MyClass2); // undefined
第二种方式定义,MyClass2 类名在类中是可以使用的,而类外部是不存在的。
类的属性和方法
访问器属性
类在构造函数创建自己属性,同时类也支持直接在原型上定义访问器属性。
var defaultInfo = { name: "ES6" };
class MyClass {
constructor (info = defaultInfo ) {
this.info = info;
}
get name () {
return this.info.name;
}
set name (value) {
this.info.name = value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(MyClass.prototype, "name");
console.log(descriptor); // Object { get: name(), set: name(), enumerable: false, configurable: true }
var example = new MyClass();
console.log(example.name); // "ES6"
example.name = "JS"
console.log(example.name); // "JS"
可计算成员名称
var name = "sayName";
class MyClass {
constructor (name) {
this.name = name;
}
[name] () {
console.log(this.name);
}
}
var example = new MyClass("ES6");
example[name]();
example.sayName();
静态成员
ES5 及早期版本中,直接将方法添加到构造函数下来模拟静态成员。ES6 类语法简化了创建静态成员的过程。
// ES6 类语法
class MyClass {
constructor (name) {
this.name = name;
}
// 静态方法
static getNumber () {
return 11111;
}
// 实例方法
sayName () {
console.log(this.name);
}
}
console.log(MyClass.getNumber()); // 11111
// ES5 版本
function Fun (name) {
this.name = name;
}
// Fun实例方法
Fun.prototype.sayName = function () {
this.name;
}
// Fun静态方法
Fun.getNumber = function () {
return 11111;
}
console.log(Fun.getNumber()); // 11111
静态成员不能在实例中访问。
继承与派生类
简单的继承
// ES6 前实现继承
function Animal (weight) {
this.weight = weight;
}
Animal.prototype.action = function () {
console.log("跑");
}
function Dog (weight) {
return Animal.call(this, weight);
}
Dog.prototype = Object.create(Animal.prototype,{
constructor: {
value: Dog,
enumerable: true,
writable: true,
configurable: true
}
});
let dog = new Dog();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
上面示例是基于 ES5 来实现继承与自定义类型的工作,看起来十分复杂:Dog 构造函数中调用 Animal.call() 方法、创建来自 Animal.prototype 的新对象并且修改该对象 [Constructor] 来赋值给 Dog 的原型。
ES6 类的出现使继承工作更容易完成。
class Animal {
constructor (weight) {
this.weight = weight;
}
action () {
console.log("跑");
}
}
class Dog extends Animal {
constructor (weight) {
super(weight);
}
}
let dog = new Dog();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
使用 extends 轻松实现继承,super() 访问基类的构造函数。
成员继承
派生类方法中与基类方法同名时,派生类会覆盖继承的基类方法。
// 基类
class SuperClass {
// 静态方法
static name () {
console.log("Super");
}
// 实例方法
sayName () {
console.log("SuperClass");
}
}
// 派生类
class SubClass extends SuperClass {
// 静态方法
static name () {
super.name();
console.log("Sub");
}
// 实例方法
sayName () {
super.sayName();
console.log("SubClass");
}
}
SubClass.name(); // 打印:Super Sub
var myClass = new SubClass();
myClass.sayName(); // 打印:SuperClass SubClass
派生自表达式的类
只要继承的表达式可以被解析为一个函数并且具有[Constructor]属性和原型,那么就可以用 extends 关键字来继承。
// 第一种
function fun (name) {
this.name = name;
}
class MyClass extends fun {
constructor (name) {
super(name);
}
sayName () {
console.log(this.name);
}
}
var myclass = new MyClass("ES6");
myclass.sayName(); // "ES6"
// 第二种
let NameMixin = {
getName () {
return this.name;
}
}
function mixin () {
let fun = function () {};
Object.assign(fun.prototype, ...arguments);
return fun;
}
class MyClass extends mixin(NameMixin) {
constructor (name) {
super();
this.name = name;
}
}
let myclass = new MyClass("ES6");
console.log(myclass.getName()); // "ES6"
第一种方式:fun 是 ES5 风格的构造函数,所以 fun 具有[Constructor]与原型,所以 MyClass 可以继承 fun。
第二种方式:mixin 来代替传统函数,接受任意数量的对象参数。首先创建 fun 函数,并且将对象参数的属性赋值给 fun 的原型上,最后返回 fun 这个函数。因此 MyClass 继承 mixin 函数返回的函数,这样做的好处:动态地确定基类上的方法。
内建对象的继承
ES5 版本中,开发者想通过继承数组方式来创建自定义的特殊数组,用传统的继承方式来实现这个操作几乎是不可能的。
// 间接调用 Array 构造函数
function newArray () {
Array.apply(this, arguments);
}
// 修改 newArray 的原型
newArray.prototype = Object.create(Array.prototype, {
constructor: {
value: newArray,
enumerable: true,
writable: true,
configurable: true
}
});
var myArray = new newArray();
myArray[5] = 1;
console.log(myArray.length); // 0
上面代码 myArray 行为表现与内建数组的不一致,这是因为通过传统 JS 继承形式实现的数组继承没有从 Array.apply() 或原型赋值中继承相关功能。
ES6 类语法支持内建对象继承,示例:
class MyArray extends Array {
}
var myarray = new MyArray();
myarray[5] = 1;
console.log(myarray.length); // 6
MyArray 与 Array 行为相似,操作数值型属性会更新 length 属性,操作 length 属性也会更新数值型数据。
上面两个代码示例体现 ES5 与 ES6 继承方式不同:
- ES5 先由派生类 newArray 创建 this 值,然后通过 apply() 调用基类 Array 的构造函数。这意味着 this 一开始就指向 newArray 的实例,然后被 Array 属性所修饰。
- ES6 先由基类 Array 创建 this 值,再由派生类构造函数 MyArray 修改 this 值。所以一开始 this 就可以访问基类 Array 的内建功能,再接收与之相关的功能。
Symbol.species
Symbol.species 是内部 Symbol 中的一个,它被用于定义返回函数的静态访问器属性,返回的函数是一个构造函数,用于实例方法中创建类的实例时必须使用这个构造函数。
// 基类
class SuperClass {
constructor () {}
static get [Symbol.species] () {
return this;
}
newObject () {
return new this.constructor[Symbol.species]();
}
}
//派生类
class SubClass1 extends SuperClass {
}
class SubClass2 extends SuperClass {
static get [Symbol.species] () {
return SuperClass;
}
}
let sub1 = new SubClass1(),
new1 = sub1.newObject(),
sub2 = new SubClass2(),
new2 = sub2.newObject();
console.log(new1 instanceof SuperClass); // true
console.log(new1 instanceof SubClass1); // true
console.log(new2 instanceof SuperClass); // true
console.log(new2 instanceof SubClass2); // false
SubClass1 继承 SuperClass 时未改变 Symbol.species 静态访问属性,由于 this.constructor[Symbol.species] 返回值是 SubClass1 ,所以 newObject() 返回 SubClass1 的实例。
SubClass2 继承 SuperClass 改变 Symbol.species 静态访问属性,由于 this.constructor[Symbol.species] 返回值是 SuperClass ,所以 newObject() 返回 SuperClass 的实例。
注意:
内建对象都有一个默认 Symbol.species 属性,该属性返回 this。
new.target
new.target 是 ES6 引入的元属性,该属性提供 new 调用信息。
class MyClass {
constructor () {
console.log(new.target === MyClass);
}
}
let myClass = new MyClass(); // true
由于 new MyClass(),创建 MyClass 实例,所以构造函数中的 new.target 为 MyClass。
可以利用 new.target 来创建抽象类,示例:
class SuperClass {
constructor () {
if (new.target === SuperClass) {
throw new Error("这个类不能实例化!");
}
}
}
class SubClass extends SuperClass {
constructor () {
super();
}
}
let myClass1 = new SubClass();
let myClass2 = new SuperClass(); // 抛出错误(Error: 这个类不能实例化!)