JavaScript 使用原型继承:每一个对象都从原型对象继承属性和方法。javascript
Java 或 Swift 等语言中做为建立对象的蓝图的传统 Class,在JavaScript 中不存在。原型继承只处理对象。前端
原型继承能够模拟经典的类继承。为了将传统的类引入 JavaScript,ES2015 标准引入了class
语法:这是在原型继承之上的一种语法糖。java
这篇文章让你熟悉 JavaScript 类:如何定义类,初始化实例,定义字段和方法,理解私有和公共字段,掌握静态字段和方法。git
JavaScript 关键字class
用于定义类:github
class User {
// The body of class
}
复制代码
上面的代码定义了一个类 User
。花括号{ }
标记 class 主体。注意,这个语法叫作 class 声明。bash
能够不指定类名,你能够经过使用 class 表达式 把 class 分配给一个变量:微信
const UserClass = class {
// The body of class
};
复制代码
您能够轻松地将 class 导出为 ES2015 模块的一部分。下面是一个“默认导出”的语法:数据结构
export default class User {
// The body of class
}
复制代码
具名导出:ide
export class User {
// The body of class
}
复制代码
当你建立一个实例时,class 就变得很是有用。实例就是包含了 class 所描述的数据和行为的一个对象。 函数
JavaScript 的new
操做符用于实例化 class :instance = new Class()
。
例如,你能够用new
操做符实例化User
类:
const myUser = new User();
复制代码
new User()
建立了 User
类的一个实例。
constructor(param1, param2, ...)
是在 class 内部初始化实例的一个特殊方法。这是设置字段初始值或进行对象设置的地方。
下面的例子就是在构造函数里设置name
字段的初始值:
class User {
constructor(name) { this.name = name; }}
复制代码
User
的构造函数有一个参数name
,用于设置字段this.name
的初始值。
构造函数内部的 this
值等于新建立的实例。
用来实例化类的参数变成了构造函数的参数:
class User {
constructor(name) {
name; // => 'Jon Snow' this.name = name;
}
}
const user = new User('Jon Snow');
复制代码
构造函数内部的name
参数的值是'Jon Snow'
。
若是不定义类的构造函数,就会建立默认构造函数。默认构造函数是一个空函数,不会修改实例。
同时,JavaScript 类最多只能有一个构造函数。
类字段是保存信息的变量。字段能够附属于两种实体:
字段有两种级别的可访问性:
让咱们看看以前的代码:
class User {
constructor(name) {
this.name = name; }
}
复制代码
表达式this.name = name
建立了一个实例字段name
并设置了初始值。
以后就能够经过属性的形式访问 name
字段:
const user = new User('Jon Snow');
user.name; // => 'Jon Snow'
复制代码
name
是一个公有字段,由于你能够在User
类外部访问到它。
当字段在构造函数中隐式建立时,就像前面的例子同样,可能很难管理字段列表。你必须从构造函数的代码中破译它们。
更好的方式是显式地声明 class 字段。不管构造函数作什么,实例老是具备相同的字段列表。
class 字段提案 容许你在 class 主体中定义字段。另外,你能够当即指定初始值:
class SomeClass {
field1;
field2 = 'Initial value';
// ...
}
复制代码
让咱们修改 User
类,声明一个公有字段name
:
class User {
name;
constructor(name) {
this.name = name;
}
}
const user = new User('Jon Snow');
user.name; // => 'Jon Snow'
复制代码
class 主体里的name;
声明了一个公有字段name
。
以这种方式声明的公共字段颇有表现力:快速查看字段声明就足以知晓类的数据结构。
并且,类字段能够在声明时当即初始化。
class User {
name = 'Unknown';
constructor() {
// No initialization
}
}
const user = new User();
user.name; // => 'Unknown'
复制代码
class 主体内的 name = 'Unknown'
声明了一个 name
字段并设置了初始值'Unknown'
。
对公有字段的访问和更新没有限制。能够在构造函数、方法以及 class 外部读取和赋值给公有字段。
封装是一个重要的概念,它能够隐藏 class 内部的细节。使用封装类的人只依赖类提供的公共接口,而不与类的实现细节耦合。
组织 class 的时候充分考虑封装,当实现细节改变的时候更新起来更容易。
隐藏对象内部数据的一种好方法是使用私有字段。这些字段只能在它们所属的类中读取和更改。类的外部不能直接更改私有字段。
私有字段 只能在 class 内部访问。
在字段名前面加上特殊字符 #
可使其变为私有,好比#myField
。每次使用该字段时,前缀#
必须保留:声明时、读取时和修改时。
让咱们确保字段 #name
能够在实例初始化时设置一次:
class User {
#name;
constructor(name) {
this.#name = name;
}
getName() {
return this.#name;
}
}
const user = new User('Jon Snow');
user.getName(); // => 'Jon Snow'
user.#name; // 抛出 SyntaxError 异常
复制代码
#name
是私有字段。你能够在 User
内部访问和修改 #name
。 getName()
方法能够访问私有字段 #name
。
可是若是你尝试从 User
类外部访问私有变量#name
,就会抛出语法错误:SyntaxError: Private field '#name' must be declared in an enclosing class
。
你也能够在 class 本身上面定义字段:静态字段。这有助于定义类常量或存储特定于该类的信息。
要在 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('Site Admin', User.TYPE_ADMIN);
admin.type === User.TYPE_ADMIN; // => true
复制代码
static TYPE_ADMIN
和static TYPE_REGULAR
在User
类内部定义了静态变量。要访问静态字段,你必须用类名加上字段名:User.TYPE_ADMIN
和User.TYPE_REGULAR
有时甚至静态字段也是你但愿隐藏的实现细节。在这里,你也能够将静态字段设为私有。
要将静态字段设为私有,只要在字段名前面加上特殊符号#
: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('Jon Snow');
new User('Arya Stark');
new User('Sansa Stark'); // throws Error
复制代码
静态字段 User.#MAX_INSTANCES
设置了容许的最大实例数量,静态字段User.#instances
是实际建立的实例数量。
私有静态字段只能在 User
类内部访问。外部范围没法干预这里的限制机制:这就是封装的好处。
字段包含了数据。可是修改数据的能力是由特殊函数提供的,它是类的一部分:方法。
JavaScript 类支持实例方法和静态方法。
实例方法能够访问和修改实例数据。实例方法能够调用其余实例方法,也能够调用任意静态方法。
例如,咱们在 User
类中定义一个 getName()
方法,用来返回 name
:
class User {
name = 'Unknown';
constructor(name) {
this.name = name;
}
getName() { return this.name; }}
const user = new User('Jon Snow');
user.getName(); // => 'Jon Snow'
复制代码
getName() { ... }
是User
类中的一个方法。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('Jon Snow');
user.nameContains('Jon'); // => true
user.nameContains('Stark'); // => 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('Jon Snow');
user.nameContains('Jon'); // => true
user.nameContains('Stark'); // => false
user.#getName(); // SyntaxError is thrown
复制代码
#getName()
是个私有方法。在方法nameContains(str)
内部,用这种方式调用私有方法:this.#getName()
。
因为是私有的,#getName()
不能在User
类外部被调用。
getter 和 setter 模拟常规字段,但对如何访问和更改字段有更多的控制。
getter 在试图获取字段值时执行,而 setter 在试图设置值时执行。
为了确保 User
的 name
属性不为空,让咱们在 getter 和 setter 中包装私有字段#nameValue
:
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('Jon Snow');
user.name; // The getter is invoked, => 'Jon Snow'
user.name = 'Jon White'; // The setter is invoked
user.name = ''; // The setter throws an Error
复制代码
get name() {...}
getter 在你访问字段 user.name
时执行。
而 set name(name) {...}
在字段更新user.name = 'Jon White'
时执行。若是新的值是空字符串,setter 就会抛出错误。
静态方法是直接附属于类的方法。它们包含了跟类相关的逻辑,而不是类的实例。
要建立静态方法,请使用特殊的关键字static
,后面加上常规的方法语法:static myStaticMethod() { ... }
使用静态方法时,须要记住两个简单的规则:
例如,咱们来建立一个静态方法,用于检测某个用户名是否被占用。
class User {
static #takenNames = [];
static isNameTaken(name) {
return User.#takenNames.includes(name);
}
name = 'Unknown';
constructor(name) {
this.name = name;
User.#takenNames.push(name);
}
}
const user = new User('Jon Snow');
User.isNameTaken('Jon Snow'); // => true
User.isNameTaken('Arya Stark'); // => false
复制代码
isNameTaken()
是个静态方法,使用了静态私有字段User.#takenNames
检查被占用的名字。
静态方法能够是私有的:static #staticFunction() {...}
。一样,它们也遵循私有规则:只能在类内部调用私有静态方法。
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
。
注意,父类的私有成员不能被子类继承。
若是你想在子类中调用父类的构造函数,你须要在子类构造函数中使用特殊的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('John Smith', ['Why I like JS']);
writer.name; // => 'John Smith'
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); }
}
复制代码
若是你想在子类方法中访问父类方法,你可使用特殊的快捷方式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 'Unknwon';
}
return name;
}
}
const writer = new ContentWriter('', ['Why I like JS']);
writer.getName(); // => 'Unknwon'
复制代码
子类 ContentWriter
中的getName()
访问了父类 User
的方法 super.getName()
。
该特性叫作方法重写.
注意,你也能够在静态方法中使用 super
,用于访问父类的静态方法。
object instanceof Class
是用来判断object
是否为 Class
实例的操做符。
咱们来看看instanceof
实际是怎么用的:
class User {
name;
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
const user = new User('Jon Snow');
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('John Smith', ['Why I like JS']);
writer instanceof ContentWriter; // => true
writer instanceof User; // => true
复制代码
writer
是子类 ContentWriter
的实例。操做符 writer instanceof ContentWriter
是值为 true
。
同时,ContentWriter
是User
的子类,所以 writer instanceof User
也是true
。
若是要判断实例的确切类要怎么作?你可使用 constructor
属性,并与 class 直接比较:
writer.constructor === ContentWriter; // => true
writer.constructor === User; // => false
复制代码
必须这样说,JavaScript 的 class 语法很好地抽象了原型继承机制。为了描述 class
语法,我甚至没用到“prototype”这个词。
可是 class 是在原型继承的基础上构建的。每一个类都是一个函数,并在做为构造函数调用时建立一个实例。
下面这两段代码是等效的。
class 版本:
class User {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
const user = new User('John');
user.getName(); // => 'John Snow'
user instanceof User; // => true
复制代码
prototype 版本:
function User(name) {
this.name = name;
}
User.prototype.getName = function() {
return this.name;
}
const user = new User('John');
user.getName(); // => 'John Snow'
user instanceof User; // => true
复制代码
若是您熟悉 Java 或 Swift 语言的经典继承机制,那么 class 语法更容易使用。
无论怎么样,即便你在 JavaScript 中使用 class 语法,我仍是推荐你好好掌握原型继承
本文提到的 class 特性出如今 ES2015 和 stage 3 提案。
到 2019 年末,class 特性分布在如下几个提案和标准中:
JavaScript 类用构造函数初始化实例、定义字段和方法。你甚至可使用static
关键字在类上面附加字段和方法
继承是经过 extends
关键字实现的:你能够轻松地从父类建立子类。super
关键字用于子类访问父类。
为了利用封装,让字段和方法变成私有以便隐藏 class 的内部细节。私有字段和方法名必须以#
开头。
JavaScript 中的类变得愈来愈方便使用了。
在私有属性前加上#
前缀,你怎么看?
做者:Dmitri Pavlutin
原文:dmitripavlutin.com/javascript-…
翻译:1024译站
更多前端技术干货尽在微信公众号:1024译站