原型继承可以模拟经典类继承。为了将传统的类引入JavaScript, ES2015 标准引入了class语法,其底层实现还是基于原型,只是原型继承的语法糖。
这篇文章将告诉你:如何定义类,初始化实例,定义字段和方法,理解私有和公共字段,掌握静态字段和方法。
1. 定义:类关键字
class User { // 类的主体 }
ES6 模块的一部分,默认导出语法如下:
export default class User { // 主体 } export class User { // 主体 }
例如,可以使用new操作符实例化User类:
const myUser = new User();
2. 初始化:constructor()
constructor(param1, param2, ...)是用于初始化实例的类主体中的一种特殊方法。在这里可以设置字段的初始值或进行任何类型的对象设置。
class User { constructor(name) { this.name = name; } }
3.字段
类字段是保存信息的变量,字段可以附加到两个实体:
-
类实例上的字段
-
类本身的字段(也称为静态字段)
字段有两种级别可访问性:
-
public:该字段可以在任何地方访问 -
private:字段只能在类的主体中访问
3.1 公共实例字段
class fields proposal 提案允许我们在类的主体中定义字段,并且可以立即指定初始值:
class SomeClass { field1; field2 = 'Initial value'; // ... }
3.2 私有实例字段
封装是一个重要的概念,它允许我们隐藏类的内部细节。使用封装类只依赖类提供的公共接口,而不耦合类的实现细节。
当实现细节改变时,考虑到封装而组织的类更容易更新。
隐藏对象内部数据的一种好方法是使用私有字段。这些字段只能在它们所属的类中读取和更改。类的外部世界不能直接更改私有字段。
私有字段只能在类的主体中访问。
class User { #name; constructor (name) { this.#name = name; } getName() { return this.#name; } } const user = new User('hong') user.getName() // => 'hong' user.#name // 抛出语法错误
#name是一个私有字段。可以在User内访问和修改#name。方法getName()可以访问私有字段#name。
但是,如果我们试图在 User 主体之外访问私有字段#name,则会抛出一个语法错误:SyntaxError: Private field '#name' must be declared in an enclosing class。
3.3 公共静态字段
class User { static TYPE_ADMIN = 'admin'; static TYPE_REGULAR = 'regular'; name; type; constructor(name, type) { this.name = name; this.type = type; } } const admin = new User('hong', User.TYPE_ADMIN); admin.type === User.TYPE_ADMIN; // => true
static TYPE_ADMIN和static TYPE_REGULAR在User类内部定义了静态变量。要访问静态字段,必须使用后跟字段名称的类:User.TYPE_ADMIN和User.TYPE_REGULAR
3.4 私有静态字段
有时,我们也想隐藏静态字段的实现细节,在时候,就可以将静态字段设为私有。
要使静态字段成为私有的,只要字段名前面加上#符号:static #myPrivateStaticField。
假设我们希望限制User类的实例数量。要隐藏实例限制的详细信息,可以创建私有静态字段:
class User { static #MAX_INSTANCES = 2; static #instances = 0; name; constructor(name) { User.#instances++; if (User.#instances > User.#MAX_INSTANCES) { throw new Error('Unable to create User instance'); } this.name = name; } } new User('张三'); new User('李四'); new User('王五'); // throws Error
静态字段User.#MAX_INSTANCES设置允许的最大实例数,而User.#instances静态字段则计算实际的实例数。
这些私有静态字段只能在User类中访问,类的外部都不会干扰限制机制:这就是封装的好处。
4.方法
4.1 实例方法
例如,定义一个方法getName(),它返回User类中的name :
class User { name = '无名氏'; constructor(name) { this.name = name; } getName() { return this.name; } } const user = new User('hong'); user.getName(); // => 'hong'
方法也可以是私有的。为了使方法私有前缀,名称以#开头即可,如下所示:
class User { #name; constructor(name) { this.#name = name; } #getName() { return this.#name; } nameContains(str) { return this.#getName().includes(str); } } const user = new User('hong'); user.nameContains('ong'); // => true user.nameContains('ll'); // => false user.#getName(); // SyntaxError is thrown
4.2 getters 和 setters
class User { #nameValue; constructor(name) { this.name = name; } get name() { return this.#nameValue; } set name(name) { if (name === '') { throw new Error(`name field of User cannot be empty`); } this.#nameValue = name; } } const user = new User('hong'); user.name; // getter 被调用, => 'hong' user.name = 'j'; // setter 被调用 user.name = ''; // setter 抛出一个错误
get name() {...} 在访问user.name会被执行。而set name(name){…}在字段更新(user.name = '前端小智')时执行。如果新值是空字符串,setter将抛出错误。
5. 继承: extends
JavaScript 中的类使用extends关键字支持单继承。
在class Child extends Parent { }表达式中,Child类从Parent继承构造函数,字段和方法。
例如,我们创建一个新的子类ContentWriter来继承父类User。
class User { name; constructor(name) { this.name = name; } getName() { return this.name; } } class ContentWriter extends User { posts = []; } const writer = new ContentWriter('John Smith'); writer.name; // => 'John Smith' writer.getName(); // => 'John Smith' writer.posts; // => []
5.1 父构造函数:`constructor()`中的`super()`
如果希望在子类中调用父构造函数,则需要使用子构造函数中可用的super()特殊函数。
例如,让ContentWriter构造函数调用User的父构造函数,以及初始化posts字段
class User { name; constructor(name) { this.name = name; } getName() { return this.name; } } class ContentWriter extends User { posts = []; constructor(name, posts) { super(name); this.posts = posts; } } const writer = new ContentWriter('hong', ['test']); writer.name; // => 'hong' writer.posts // => ['test']
子类ContentWriter中的super(name)执行父类User的构造函数。
注意,在使用this关键字之前,必须在子构造函数中执行super()。调用super()确保父构造函数初始化实例。
class Child extends Parent { constructor(value1, value2) { //无法工作 this.prop2 = value2; super(value1); } }
5.2 父实例:方法中的`super`
如果希望在子方法中访问父方法,可以使用特殊的快捷方式super。
class User { name; constructor(name) { this.name = name; } getName() { return this.name; } } class ContentWriter extends User { posts = []; constructor(name, posts) { super(name); this.posts = posts; } getName() { const name = super.getName(); if (name === '') { return '无名氏'; } return name; } } const writer = new ContentWriter('hong', ['test']); writer.getName(); // => '无名氏'
子类ContentWriter的getName()直接从父类User访问方法super.getName(),这个特性称为方法重写。
注意,也可以在静态方法中使用super来访问父类的静态方法。
6. 类和原型
必须说 JS 中的类语法在从原型继承中抽象方面做得很好。但是,类是在原型继承的基础上构建的。每个类都是一个函数,并在作为构造函数调用时创建一个实例。
以下两个代码段是等价的。
类版本:
class User { constructor(name) { this.name = name; } getName() { return this.name; } } const user = new User('hong'); user.getName(); // => 'hong' user instanceof User; // => true
使用原型的版本:
function User(name) { this.name = name; } User.prototype.getName = function() { return this.name; } const user = new User('hong'); user.getName(); // => 'hong' user instanceof User; // => true
浙公网安备 33010602011771号