您的位置:首页 > Web前端 > JavaScript

你需要知道的 JavaScript 类(class)的这些知识

2020-01-11 08:00 706 查看

作者: Dmitri Pavlutin

译者:前端小智

来源:dmitripavlutin

点赞再看,养成习惯

本文

GitHub
https://github.com/qq449245884/xiaozhi 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

JavaScript 使用原型继承:每个对象都从原型对象继承属性和方法。

Java
Swift
等语言中使用的传统类作为创建对象的蓝图,在 JavaScript 中不存在,原型继承仅处理对象。

原型继承可以模拟经典类继承。为了将传统的类引入JavaScript, ES2015 标准引入了

class
语法,其底层实现还是基于原型,只是原型继承的语法糖。

这篇文章主要让你熟悉 JavaScript 类:如何定义类,初始化实例,定义字段和方法,理解私有和公共字段,掌握静态字段和方法。

1. 定义:类关键字

使用关键字

class
可以在 JS 中定义了一个类:

class User {
// 类的主体
}

上面的代码定义了一个

User
类。 大括号
{}
里面是类的主体。 此语法称为
class
声明。

如果在定义类时没有指定类名。可以通过使用

类表达式
,将类分配给变量:

const UserClass = class {
// 类的主体
}

还可以轻松地将类导出为 ES6 模块的一部分,默认导出语法如下:

export default class User {
// 主体
}

命名导出如下:

export class User {
// 主体
}

当我们创建类的实例时,该类将变得非常有用。实例是包含类所描述的数据和行为的对象。

使用

new
运算符实例化该类,语法:
instance = new Class()

例如,可以使用

new
操作符实例化
User
类:

const myUser = new User();

new User()
创建
User
类的一个实例。

2. 初始化:constructor()

constructor(param1, param2, ...)
是用于初始化实例的类主体中的一种特殊方法。 在这里可以设置字段的初始值或进行任何类型的对象设置。

在下面的示例中,构造函数设置字段

name
的初始值

class User {
constructor(name) {
this.name = name;
}
}

User
的构造函数有一个参数
name
,用于设置字段
this.name
的初始值

在构造函数中,

this
值等于新创建的实例。用于实例化类的参数成为构造函数的参数:

class User {
constructor(name) {
name; // => '前端小智'
this.name = name;
}
}

const user = new User('前端小智');

构造函数中的

name
参数的值为
'前端小智'
。如果没有定义该类的构造函数,则会创建一个默认的构造函数。默认的构造函数是一个空函数,它不修改实例。

同时,一个JavaScript 类最多可以有一个构造函数。

3.字段

类字段是保存信息的变量,字段可以附加到两个实体:

  1. 类实例上的字段
  2. 类本身的字段(也称为静态字段)

字段有两种级别可访问性:

  1. public
    :该字段可以在任何地方访问
  2. private
    :字段只能在类的主体中访问

3.1 公共实例字段

让我们再次看看前面的代码片段:

class User {
constructor(name) {
this.name = name;
}
}

表达式

this.name = name
创建一个实例字段名,并为其分配一个初始值。然后,可以使用属性访问器访问
name
字段

const user = new User('前端小智');
user.name; // => '前端小智'

name
是一个公共字段,因为你可以在
User
类主体之外访问它。

当字段在构造函数中隐式创建时,就像前面的场景一样,可能获取所有字段。必须从构造函数的代码中破译它们。

class fields proposal
提案允许我们在类的主体中定义字段,并且可以立即指定初始值:

class SomeClass {
field1;
field2 = 'Initial value';

// ...
}

接着我们修改

User
类并声明一个公共字段
name

class User {
name;

constructor(name) {
this.name = name;
}
}

const user = new User('前端小智');
user.name; // => '前端小智'

name;
在类的主体中声明一个公共字段
name

以这种方式声明的公共字段具有表现力:快速查看字段声明就足以了解类的数据结构,而且,类字段可以在声明时立即初始化。

class User {
name = '无名氏'

constructor () {
}
}

const user = new User();
user.name; // '无名氏'

类体内的

name ='无名氏'
声明一个字段名称,并使用值
'无名氏'
对其进行初始化。

对公共字段的访问或更新没有限制。可以读取构造函数、方法和类外部的公共字段并将其赋值。

3.2 私有实例字段

封装是一个重要的概念,它允许我们隐藏类的内部细节。使用封装类只依赖类提供的公共接口,而不耦合类的实现细节。

当实现细节改变时,考虑到封装而组织的类更容易更新。

隐藏对象内部数据的一种好方法是使用私有字段。这些字段只能在它们所属的类中读取和更改。类的外部世界不能直接更改私有字段。

私有字段只能在类的主体中访问。

在字段名前面加上特殊的符号

#
使其成为私有的,例如
#myField
。每次处理字段时都必须保留前缀
#
声明它、读取它或修改它。

确保在实例初始化时可以一次设置字段

#name

class User {
#name;

constructor (name) {
this.#name = name;
}

getName() {
return this.#name;
}
}

const user = new User('前端小智')
user.getName() // => '前端小智'

user.#name  // 抛出语法错误

#name
是一个私有字段。可以在
User
内访问和修改
#name
。方法
getName()
可以访问私有字段
#name

但是,如果我们试图在

User
主体之外访问私有字段
#name
,则会抛出一个语法错误:
SyntaxError: Private field '#name' must be declared in an enclosing class

3.3 公共静态字段

我们还可以在类本身上定义字段:

静态字段
。这有助于定义类常量或存储特定于该类的信息。

要在 JavaScript 类中创建静态字段,请使用特殊的关键字

static
后面跟字段名:
static myStaticField

让我们添加一个表示用户类型的新字段

type
:
admin
regular
。静态字
TYPE_ADMIN
TYPE_REGULAR
是区分用户类型的常量:

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('前端小智', 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.方法

字段保存数据,但是修改数据的能力是由属于类的一部分的特殊功能实现的:方法

JavaScript 类同时支持实例和静态方法。

4.1 实例方法

实例方法可以访问和修改实例数据。实例方法可以调用其他实例方法,也可以调用任何静态方法。

例如,定义一个方法

getName()
,它返回
User
类中的
name

class User {
name = '无名氏';

constructor(name) {
this.name = name;
}

getName() {
return this.name;
}
}

const user = new User('前端小智');
user.getName(); // => '前端小智'

getName() { ... }
User
类中的一个方法,
getname()
是一个方法调用:它执行方法并返回计算值(如果存在的话)。

在类方法和构造函数中,

this
值等于类实例。使用
this
来访问实例数据:
this.field
或者调用其他方法:
this.method()

接着我们添加一个具有一个参数并调用另一种方法的新方法名称

nameContains(str)

class User {
name;

constructor(name) {
this.name = name;
}

getName() {
return this.name;
}

nameContains(str) {
return this.getName().includes(str);
}
}

const user = new User('前端小智');
user.nameContains('前端');   // => true
user.nameContains('大冶'); // => false

nameContains(str) { ... }
User
类的一种方法,它接受一个参数
str
。 不仅如此,它还执行实例
this.getName()
的方法来获取用户名。

方法也可以是私有的。 为了使方法私有前缀,名称以#开头即可,如下所示:

class User {
#name;

constructor(name) {
this.#name = name;
}

#getName() {
return this.#name;
}

nameContains(str) {
return this.#getName().includes(str);
}
}

const user = new User('前端小智');
user.nameContains('前端');   // => true
user.nameContains('大冶'); // => false

user.#getName(); // SyntaxError is thrown

#getName()
是一个私有方法。在方
法nameContains(str)
中,可以这样调用一个私有方法:
this.#getName()

由于是私有的,

#getName()
不能在用
User
类主体之外调用。

4.2 getters 和 setters

getter
setter
模仿常规字段,但是对如何访问和更改字段具有更多控制。在尝试获取字段值时执行
getter
,而在尝试设置值时使用
setter

为了确保

User
name
属性不能为空,我们将私有字段
#nameValue
封装在
getter
setter
中:

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('前端小智');
user.name; // getter 被调用, => '前端小智'
user.name = '王大冶'; // setter 被调用

user.name = ''; // setter 抛出一个错误

get name() {...}
在访问
user.name
会被执行。而
set name(name){…}
在字段更新(
user.name = '前端小智'
)时执行。如果新值是空字符串,
setter
将抛出错误。

4.3 静态方法

静态方法是直接附加到类的函数,它们持有与类相关的逻辑,而不是类的实例。

要创建一个静态方法,请使用特殊的关键字

static
和一个常规的方法语法:
static myStaticMethod() { ... }

使用静态方法时,有两个简单的规则需要记住:

  1. 静态方法可以访问静态字段。
  2. 静态方法不能访问实例字段。

例如,创建一个静态方法来检测是否已经使用了具有特定名称的用户。

class User {
static #takenNames = [];

static isNameTaken(name) {
return User.#takenNames.includes(name);
}

name = '无名氏';

constructor(name) {
this.name = name;
User.#takenNames.push(name);
}
}

const user = new User('前端小智');

User.isNameTaken('前端小智');   // => true
User.isNameTaken('王大冶'); // => false

isNameTaken()
是一个使用静态私有字段
User
的静态方法用于检查已取的名字。

静态方法可以是私有的:

static #staticFunction() {...}
。同样,它们遵循私有规则:只能在类主体中调用私有静态方法。

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;     // => []

ContentWriter
继承了
User
的构造函数,方法
getName()
和字段
name
。同样,
ContentWriter
类声明了一个新的字段
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('前端小智', ['Why I like JS']);
writer.name; // => '前端小智'
writer.posts // => ['Why I like JS']

子类

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('前端小智', ['Why I like JS']);
writer.getName(); // => '无名氏'

子类

ContentWriter
getName()
直接从父类
User
访问方法
super.getName()
,这个特性称为
方法重写

注意,也可以在静态方法中使用

super
来访问父类的静态方法。

6.对象类型检查:
instanceof

object instanceof Class
是确定
object
是否为
Class
实例的运算符,来看看示例:

class User {
name;

constructor(name) {
this.name = name;
}

getName() {
return this.name;
}
}

const user = new User('前端小智');
const obj = {};

user instanceof User; // => true
obj instanceof User; // => false

user
User
类的一个实例,
user instanceof User
的计算结果为
true

空对象

{}
不是
User
的实例,相应地
obj instanceof User
false

instanceof
是多态的:操作符检测作为父类实例的子类。

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('前端小智', ['Why I like JS']);

writer instanceof ContentWriter; // => true
writer instanceof User;          // => true

writer
是子类
ContentWriter
的一个实例。运算符
writer instanceof ContentWriter
的计算结果为
true

同时

ContentWriter
User
的子类。因此
writer instanceof User
结果也为
true

如果想确定实例的确切类,该怎么办?可以使用构造函数属性并直接与类进行比较

writer.constructor === ContentWriter; // => true
writer.constructor === User;          // => false

7. 类和原型

必须说 JS 中的类语法在从原型继承中抽象方面做得很好。但是,类是在原型继承的基础上构建的。每个类都是一个函数,并在作为构造函数调用时创建一个实例。

以下两个代码段是等价的。

类版本:

class User {
constructor(name) {
this.name = name;
}

getName() {
return this.name;
}
}

const user = new User('前端小智');

user.getName();       // => '前端小智'
user instanceof User; // => true

使用原型的版本:

function User(name) {
this.name = name;
}

User.prototype.getName = function() {
return this.name;
}

const user = new User('前端小智');

user.getName();       // => '前端小智'
user instanceof User; // => true

如果你熟悉

Java
Swift
语言的经典继承机制,则可以更轻松地使用类语法。

8. 类的可用性

这篇文章中的类的一些特性有些还在分布第三阶段的提案中。在

2019
年底,类的特性分为以下两部分:

9. 总结

JavaScript 类用构造函数初始化实例,定义字段和方法。甚至可以使用

static
关键字在类本身上附加字段和方法。

继承是使用

extends
关键字实现的:可以轻松地从父类创建子类,
super
关键字用于从子类访问父类。

要利用封装,将字段和方法设为私有以隐藏类的内部细节,私有字段和方法名必须以

#
开头。

你对使用

#前
缀私有属性有何看法,欢迎留言讨论?

原文:https://dmitripavlutin.com/javascript-classes-complete-guide/

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

  • 点赞
  • 收藏
  • 分享
  • 文章举报
前端小智@大迁世界 博客专家 发布了60 篇原创文章 · 获赞 1588 · 访问量 9万+ 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: