ES6学习笔记8--JS的类

  |  

前言

在ES6之前,我们一直通过各种方式模拟传统面向对象语言中的类的概念,而在ES6版本中,JS提出了属于自己的类的概念


9


ES5中的仿类结构

JS在ES5及更早版本中都不存在类。与类最接近的是:创造一个构造器,然后将方法指派到该构造器的原型上。这种方式通常被称为创建一个自定义类型。例如

1
2
3
4
5
6
7
8
function PersoType(name) {
this.name = name;
}
PersonType.prototype.sayName = function() {
console.log(this.name);
}
let person = new PersonType("Nicholas");
person.sayName();

类的声明

类在ES6中最简单的形式就是类声明,它看起来很像其它语言中的类。

基本的类声明

类声明以class关键字开始,其后是类的名称

1
2
3
4
5
6
7
8
9
10
11
class PersonClass {
// 等价于PersonType构造器
constructor(name) {
this.name = name;
}
// 等价于PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
}
let person = new PersonClass("Nicholas");

类声明允许你在其中使用特殊的constructor方法名称直接定义一个构造器,而不需要先定义一个函数再把它当作构造器使用。建议在构造器函数内创建所有可能出现的自有属性,这样在类中声明变量就会被限制在单一位置

相比于已有的自定义类声明方式来说,类声明仅仅是以它为基础的一个语法糖。PersonClass声明实际上创建了一个拥有constructor方法及其行为的函数,这也是typeof PersonClass会得到function结果的原因

为何要使用类的语法

尽管类与自定义类型有很多相似性,但是他们之间仍有一些重要的区别:

  • 类声明不会被提升而自定义类型的函数会被提升。程序在执行到达声明处之前,雷辉存在于暂时性死区内
  • 类声明中的所有代码会自动运行在严格模式下,并且也无法退出
  • 类的所有方法都是不可枚举的,后者则必须使用Object.defineProperty()
  • 类的所有方法颞部都没有[[Construct]],因此使用new 来调用它们会抛出错误
  • 调用类构造器时不适用new,会抛出错误
  • 试图在类的方法内部重写类名,会抛出错误

就上面的这些去背来看,实际上声明可以等价为一下未使用类语法的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let PersonType2 = (function() {
"use strict";
const PersonType2 = function(name) {
if(typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.name = name;
}
Object.definePropterty(PersonType2.prototype, "sayName", {
value: function() {
if(typeof new.target !== "undefined") {
throw new Error("Method cannot be called with new.");
}
console.log(this.name)
},
enumerable: false,
wirtable: true,
configurable: true
});
return PersonType2;
})();

类表达式

类与函数有相似指出,即它们都有两种形式:声明与表达式。函数声明与类声明都以适当的关键词起始,随后是标识符。函数具有表达式形式,无需在function后使用标识符;类似的,类也有不需要标识符的表达式形式。

基本的类表达式

1
2
3
4
5
6
7
8
9
let PersonClass = class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
let person = new PersonClass("Nicholas");

使用类声明还是类表达式,主要还是根据代码风格。相对于函数声明和函数表达式之间的区别,类声明和类表达式都不进行提升,因此对代码运行并没有本质上的影响

具名类表达式

1
2
3
4
5
6
7
8
let PersonClass = class PersonClass2 {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}

在这个例子中,类表达式被命名为PersonClass2。但是这个表示符号只能在类定义中存在,也只能在类的内部使用。

作为一级公民的类

在编程中,能被当作值来使用的就被成为一级公民,以为这他能作为参数传递给函数、能作为函数的返回值、能用来给变量赋值。

ES6延续了传统,让类也成为了一级公民。这使得类可以被多种方式使用。

例如,他能作为参数传入函数

1
2
3
4
5
6
7
8
9
function createObject(classDef) {
return new classDef();
}
let obj = createObject(class {
sayHi() {
console.log('hi')
}
});
obj.sayHi(); // 'hi'

类表达式的另一个用法就是立即调用类构造器,用来创建单例,但这需要new关键字的配合

1
2
3
4
5
6
7
8
9
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("Nicholas");
person.sayName(); // 'Nicholas'

匿名类表达式可以用来创建单例而不留下任何可以被查探的类引用。

访问器属性

类允许在原型上定义访问器属性。为了创建一个getter,需要使用get关键字,并要与后方标识符之间留出空格;创建setter使用相同的方法,只是需要使用set关键字

1
2
3
4
5
6
7
8
9
10
11
12
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value
}
}
let descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, "html");

如果我们使用非类的等价表示,那么就如同下列代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let CustomHTMLElement = (function() {
"use strict";
const CustomHTMLElement = function(element) {
if(typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.element = element;
}
Object.defineProperty(CustomHTML.prototype, "html", {
enumerable: false,
configurable: true,
get: function() {
return this.element.innerHTML;
},
set: function(value) {
this.element.innerHTML = value;
}
})
return CustomHTMLElement;
})();

使用类语法能够少些大量的代码

需计算的成员名

类方法与类访问器属性也能够使用需计算的名称。无需使用标识符,只要用方括号来包裹一个表达式

1
2
3
4
5
6
7
8
9
10
11
let methodName = "sayName";
class PersonClass {
constructor(name) {
this.name = name;
}
[methodName]() {
console.log(this.name);
}
}
let me = new PersonClass("Nicholas");
me.sayName(); // "Nicholas"

访问器属性能以相同方式使用需计算的名称

1
2
3
4
5
6
7
8
9
10
11
12
let propertyName = "html";
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get [propertyName]() {
return this.element.innerHTML;
}
set [propertyName](value) {
this.element.innerHTML = value;
}
}

生成器方法

我们同样可以在类中定义生成器方法

1
2
3
4
5
6
7
class MyClass {
*createIterator() {
yield 1;
yield 2;
yield 3;
}
}

除此之外我们还可以使用知名符号Symbol.iterator来为自定义类定义默认迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Collection {
constructor() {
this.items = [];
}
*[Symbol.iterator]() {
yield *this.items.values();
}
}
let collection = new Collection();
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for(let x of collection) {
console.log(x);
}

静态成员

直接在构造器上添加额外方法来模拟静态成员,这在ES5及其早期版本中是另一个通用的模式

1
2
3
4
5
6
7
8
function PersonType(name) {
this.name = name;
}
//静态方法
PersonType.create = function(name) {
return new PersonType(name);
}
let person = PersonType.create("Nicholas");

在其它编程语言中,工厂方法PersonType.create()会被认定为一个静态方法,它的数据不依赖PersonType的任何实例,且无法被任何实例调用。ES6的类简化了静态成员的创建,只要在方法与访问器属性的名称之前添加正式的static标注

1
2
3
4
5
6
7
8
9
class PersonClass {
constructor(name) {
this.name = name;
}
static create(name) {
return new PersonClass(name);
}
}
let person = PersonClass.create("Nicholas");

可以在类的任何方法与访问器属性上使用static关键字,但是不能用于constructor方法

使用派生类进行继承

在ES6之前,实现自定义类型的继承是一个繁琐的过程。严格的继承需要经历多个步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
}
function Square(length) {
Rectangle.call(this, length, length);
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
value: Square,
enumerable: true,
writable: true,
configurable: true
}
});
var square = new Square(3);
console.log(square.getArea());
console.log(square instanceof Square);
console.log(square instanceof Rectanagle);

这是一个典型的寄生组合式继承,通过构造函数的调用加父原型的浅拷贝实现的继承。

类让继承工作变的更为轻松,使用其它面向对象语言熟悉的extends关键字来指定当前类所需要继承的类,即可。生成的类的原型会被自动调整,而且可以在子类中使用super()方法来调用夫类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
let square = new Square(3)

在这个例子中Square类使用extends关键字继承了Rectangle。

继承了其它类的类被称为派生类。如果派生类制定了构造器,就需要使用super(),否则会导致错误。如果不使用构造器,super()方法会被自动调用,并会使用创建新实例时提供的所有参数

1
2
3
4
5
class Square extends Rectangle {
constructor(..args) {
super(..args);
}
}

使用super有几个需要注意的点:

  • 只能在派生类中使用super
  • 在构造器中,你必须在访问this之前调用super。由于super负责初始化this,因此试图像访问this自然会导致错误
  • 唯一能避免调用super()的方法,是从类构造器中返回一个对象

对最后一个点做出一下解释。如果不想调用super,那么只能在子类的构造函数中返回一个对象

1
2
3
4
5
6
7
8
9
10
class A {
constructor() {
this.name = 'test'
}
}
class B extends A {
constructor() {
return {awful: true};
}
}

这表面上看来B是继承了A,但是B已经起不到类的作用了。

屏蔽类方法

派生类中的方法总是会屏蔽父类的同名方法。但是我们可以使用super来调用父类中的同名方法

1
2
3
4
5
6
7
8
class Square extends Rectangle {
constructor(length){
super(length, length);
}
getArea() {
return super.getArea();
}
}

继承静态成员

如果父类包含静态成员,呢么这些静态成员在派生类中也是可以使用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Rectangele {
constructor(length, width) {
this.length = length;
this.width = width;
}
static create(length, width) {
return new Rectangle(length, width);
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
let rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Square); // false

从表达式中派生类

在ES6中派生类的最强大能力,就是能够从表达式中派生类。只要一个表达式能够返回一个具有[[Constructor]]属性以及原型的函数,就能对其使用extends

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
}
function getBase() {
return Rectangle;
}
class Square extends getBase() {
console.log(length) {
super(length, length);
}
}
let x = new Square(3);
console.log(x instanceof Rectangle); // true

通过这种方式我们能实现动态决定父类,也能创建不同的继承方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
}
let AreaMixin = {
getArea() {
return this.length * this.width;
}
}
function mixin(...mixins) {
let base = function() { this.base = "base"; };
Object.assign(base.prototype, ...mixins);
return base
}
class Square extends mixin(AreaMixin, SerializableMixin) {
constructor(length) {
super();
this.length = length;
this.width = length;
}
}
let x = new Square(3);

mixin

如果多个混入对象拥有相同的属性,则只有最后添加的属性会被保留

继承内置对象

在ES5的传统继承中,this的值会先被派生类创建,虽有父类构造器才被调用。这也就意味着this一开始就是派生类的实例,之后才能使用父类的附加属性对其进行装饰

在ES6基于类的继承中,this的值会先被父类创建,随后才被派生类的构造器所修改。结果是this初始就会拥有作为父类的内置对象(Array, Object….)的所有功能,并能正确接收与之关联的所有功能

Symbol.species属性

继承内置对象有一个独特的点,任意会返回内置对象实例的方法,在派生类上会返回派生类的实例

1
2
3
4
class MyArray extends Array {}
let items = new MyArray(1,2,3,4)
subitems = items.slice(1,2);
console.log(subitems instanceof MyArray); //true

这种独特情况是Symbol.species属性在后台实现的

Symbol.species知名符号被用于定义一个能返回函数的静态访问器属性。每当类实例的方法(构造器除外)必须创建一个实例时,静态访问器返回的函数就会被用来创建新实例。下列内置类型都定义了Symbol.species

  • Array
  • ArrrayBuffer
  • Map
  • Promise
  • RegExp
  • Set
  • 类型化数组

以上每个类型都拥有默认的 Symbol.species 属性,其返回值为 this ,意味着该属性总是会返回自身的构造器函数。若你准备在一个自定义类上实现此功能,代码就像这样:

1
2
3
4
5
6
7
8
9
10
11
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}

这里开始我有一个疑问,就是为什么Symbol.species会属于类中的构造器方法。后来写了个示例

constructor

其实会有这样的疑问还是因为对类实例化过程中this的指向记的不太熟的关系。一开始我下意识的觉得this指向的是MyClass,那么this.constructor不就是类的构造器吗?其实并不是,当我们用MyClass类创建一个实例后,this就绑定为实例,譬如这里的test.那么test的原型会有一个constructor属性,指向类的创建函数。所以这里实际上this.constructor就是MyClass,所以我们这里调用的是类的静态方法。

写到一般,我又有了一个疑问,静态访问器属性中的this指向什么。有这个疑问主要还是因为我对类的本质理解还不够透彻

ES6中,类其实还是函数,那么类中的构造器初始化的是实例自身的属性和方法,普通方法是实例原型上的方法。那么静态成员就是类这个函数自身的属性和方法,那么类自身的属性和方法中的this当然指向类本身

由于我们这里返回的是this,所以在继承的过程中它是会动态改变的,当然我们可以在派生类中重写这个访问器属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
class MyDerivedClass1 extends MyClass {}
class MyDerivedClass2 extends MyClass {
static get [Symbol.species]() {
return MyClass;
}
}
let instance1 = new MyDerivedClass1(1),
clone1 = instance1.clone(),
instance2 = new MyDerivedClass2(2),
clone2 = instance2.clone();
console.log(clone1 instanceof MyClass); // true
console.log(clone1 instanceof MyDerivedClass1); // true
console.log(clone2 instanceof MyClass); // true
console.log(clone2 instanceof MyDerivedClass2); // false

一般而言,每当想在类方法中使用 this.constructor 时,你就应当设置类的Symbol.species 属性。这么做允许派生类轻易地重写方法的返回类型。此外,若你从一个拥有 Symbol.species 定义的类创建了派生类,要保证使用此属性,而不是直接使用类构造器。

在类构造器中使用new.target

我们知道通过new.target属性可以知道当前被new调用的类构造器。通过这个方法我们可以创建一个抽象父类(及一种不能被实例化的类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Shape {
constructor() {
if(new.target === Shape) {
throw new Error('This class cannot be instantiated directly');
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
文章目录
  1. 1. ES5中的仿类结构
  2. 2. 类的声明
    1. 2.1. 基本的类声明
    2. 2.2. 为何要使用类的语法
  3. 3. 类表达式
    1. 3.1. 基本的类表达式
    2. 3.2. 具名类表达式
  4. 4. 作为一级公民的类
  5. 5. 访问器属性
  6. 6. 需计算的成员名
  7. 7. 生成器方法
  8. 8. 静态成员
  9. 9. 使用派生类进行继承
    1. 9.1. 屏蔽类方法
    2. 9.2. 继承静态成员
    3. 9.3. 从表达式中派生类
    4. 9.4. 继承内置对象
    5. 9.5. Symbol.species属性
  10. 10. 在类构造器中使用new.target
|