做者:Dmitri Pavlutin翻译:疯狂的技术宅javascript
原文:https://dmitripavlutin.com/ja...前端
未经容许严禁转载java
JavaScript 使用原型继承:每一个对象都从其原型对象继承属性和方法。git
在 JavaScript 中不存在 Java 或 Swift 等语言中所使用的做为建立对象 蓝图的传统类,原型继承仅处理对象。程序员
原型继承能够模仿经典类的继承。为了将传统类引入 JavaScript,ES2015 标准引入了 class
语法:基于原型继承上的语法糖。github
本文使你熟悉 JavaScript 类:如何定义类,初始化实例,定义字段和方法,了解私有字段和公共字段,掌握静态字段和方法。面试
用特殊关键字 class
在 JavaScript 中定义一个类:segmentfault
class User { // The body of class }
上面的代码定义了一个类 User
。大括号 { }
界定了类的主体。请注意,此语法称为 类声明 。服务器
你没有义务指明 class 的名称。经过使用类表达式 ,你能够将类分配给变量:微信
const UserClass = class { // The body of class };
能够轻松地将类导出为 ES2015 模块的一部分。这是 默认导出 的语法:
export default class User { // The body of class }
还有一个 命名导出 :
export class User { // The body of class }
当你建立类的 实例(instance) 时,该类将变得颇有用。实例是一个包含类描述的数据和行为的对象。
new
运算符可在 JavaScript 中实例化该类:instance = new Class()
。
例如,你能够用 new
运算符实例化 User
类:
const myUser = new User();
new User()
建立 User
类的实例。
constructor(param1,param2,...)
是类中初始化实例的特殊方法。在这里你能够设置字段的初始值或针对对象进行任何类型的设置。
在如下示例中,构造函数设置了字段 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 类最多能够有一个构造函数。
类字段是用来保存信息的变量。字段能够附加到 2 个实体:
这些字段还具备 2 级可访问性:
让咱们再次看一下以前的代码片断:
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 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'
类中的 name;
声明了一个公共字段 name
。
以这种方式声明的公共字段有很好的表现力:经过查看字段声明就可以了解该类的数据结构。
并且,能够在声明时当即初始化类字段。
class User { name = 'Unknown'; constructor() { // No initialization } } const user = new User(); user.name; // => 'Unknown'
类中的 name ='Unknown'
声明一个字段 name
并用值 'Unknown'
初始化它。
对公有字段的访问或更新没有任何限制。你能够读取它们的值并将其分配给构造函数、方法内部以及类外部的公有字段。
封装是一个重要的概念,可以让你隐藏类的内部细节。使用封装类的人仅涉及该类提供的公共接口,而不会耦合到该类的实现细节。
当实现细节被更改时,考虑封装性的类更易于更新。
使用私有字段是隐藏对象内部数据的一种好方法。这是只能在它们所属的类中读取和修改的字段。该类的外部不能直接更改私有字段。
私有字段 仅可在类的正文中访问。
在字段名前加上特殊符号 #
使其私有,例如 #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 is thrown
#name
是一个私有字段。你能够在 User
主体内访问和修改 #name
。方法 getName()
能够访问私有字段 #name
。
若是尝试在用户类主体以外访问私有字段 #name
,则会引起语法错误:SyntaxError: Private field '#name' must be declared in an enclosing 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
静态 TYPE_ADMIN
和静态 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 类支持实例方法和静态方法。
实例方法能够访问和修改实例数据。实例方法能够调用其余实例方法以及任何静态方法。
例如,让咱们定义一个方法 getName()
,该方法返回 User
类中的名称:
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()
。
让咱们添加一个新方法 name Contains(string)
,该方法有一个参数并调用另外一个方法:
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()
的另外一种方法来获取用户名。
方法也能够是私有的。能够经过前缀使方法私有,其名称以#
开头。
让咱们将 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()
。
做为私有变量,不能在 User
类主体以外调用 #getName()
。
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('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
当你访问字段 user.name
的值时,将执行 get name() {...}
getter。
在 set name(name){...}
字段 user.name ='Jon White'
更新时执行。若是新值是一个空字符串,则 setter 将引起错误。
静态方法是直接附加到类的函数。它们具备与类相关的逻辑,而不是与类的实例相关的逻辑。
要建立静态方法,请使用特殊关键字 static
,后跟常规方法语法:static myStaticMethod() { ... }
。
使用静态方法时,要记住两个简单的规则:
让咱们建立一个静态方法来检测是否已经使用了具备特定名称的 User。
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
从父类继承构造函数字段和方法。
例如,让咱们建立一个新的子类 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) { // Does not work! 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
属性并直接与该类进行比较:
writer.constructor === ContentWriter; // => true writer.constructor === User; // => false
我必须说,JavaScript 中的类语法在从原型继承中进行抽象方面作得很好。为了描述 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
使用原型的版本:
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 语言的经典继承机制,则能够更轻松地使用类语法。
无论怎样,即使是你在 JavaScript 中使用类语法,我也建议你对原型继承有所了解。
本文中介绍的课程功能涉及 ES2015 和第 3 阶段的提案。
在2019年末,class 功能分为如下两部分:
JavaScript 类用构造函数初始化实例,定义字段和方法。你甚至可使用 static
关键字在类自己上附加字段和方法。
继承是使用 extends
关键字实现的:你能够轻松地从父级建立子级。super
关键字用于从子类访问父类。
要使用封装,请将字段和方法设为私有来隐藏类的内部细节。私有字段和方法名称必须以 #
开头。
JavaScript 中的类正在变得愈来愈易于使用。