在面向对象编程中,类是一个可扩展的程序代码模板,用于创建对象,为状态(成员变量)提供初始值,并实现行为(成员函数或方法)。
在实践中,我们经常需要创建许多同类对象,如用户、商品或其他任何东西。
正如我们在 构造函数,运算符“new” 一章中所了解的,new function
可以帮助我们实现这一点。
但在现代 JavaScript 中,有一个更高级的“类”构造,它引入了许多对面向对象编程有用的新特性。
“类”语法
基本语法是
class MyClass {
// class methods
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
然后使用 new MyClass()
创建一个包含所有已列出方法的新对象。
constructor()
方法由 new
自动调用,因此我们可以在其中初始化对象。
例如
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}
// Usage:
let user = new User("John");
user.sayHi();
当调用 new User("John")
时
- 将创建一个新对象。
constructor
随给定参数运行,并将其分配给this.name
。
…然后我们可以调用对象方法,例如 user.sayHi()
。
新手开发人员常犯的一个错误是在类方法之间放置逗号,这会导致语法错误。
此处的符号不要与对象字面量混淆。在类中,不需要逗号。
什么是类?
那么,class
到底是什么?这并不是一个全新的语言级实体,正如人们可能认为的那样。
让我们揭开任何魔法,看看类到底是什么。这将有助于理解许多复杂方面。
在 JavaScript 中,类是一种函数。
在此,请看
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// proof: User is a function
alert(typeof User); // function
class User {...}
构造真正做的是
- 创建一个名为
User
的函数,该函数成为类声明的结果。函数代码取自constructor
方法(如果我们不编写此类方法,则假设为空)。 - 将类方法(例如
sayHi
)存储在User.prototype
中。
创建 new User
对象后,当我们调用其方法时,它将从原型中获取,正如 F.prototype 章节中所述。因此,对象可以访问类方法。
我们可以将 class User
声明的结果说明为
以下是用于内省它的代码
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// class is a function
alert(typeof User); // function
// ...or, more precisely, the constructor method
alert(User === User.prototype.constructor); // true
// The methods are in User.prototype, e.g:
alert(User.prototype.sayHi); // the code of the sayHi method
// there are exactly two methods in the prototype
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
不仅仅是语法糖
有时人们说 class
是“语法糖”(旨在使内容更易于阅读的语法,但不会引入任何新内容),因为我们实际上可以在不使用 class
关键字的情况下声明相同的内容
// rewriting class User in pure functions
// 1. Create constructor function
function User(name) {
this.name = name;
}
// a function prototype has "constructor" property by default,
// so we don't need to create it
// 2. Add the method to prototype
User.prototype.sayHi = function() {
alert(this.name);
};
// Usage:
let user = new User("John");
user.sayHi();
此定义的结果大致相同。因此,确实有理由可以将 class
视为一种语法糖,用于定义一个构造函数及其原型方法。
尽管如此,还是有重要的区别。
-
首先,由
class
创建的函数由特殊内部属性[[IsClassConstructor]]: true
标记。因此,它与手动创建它并不完全相同。语言在许多地方检查该属性。例如,与常规函数不同,它必须用
new
调用class User { constructor() {} } alert(typeof User); // function User(); // Error: Class constructor User cannot be invoked without 'new'
此外,大多数 JavaScript 引擎中类构造函数的字符串表示以“class…”开头
class User { constructor() {} } alert(User); // class User { ... }
还有其他区别,我们很快就会看到它们。
-
类方法不可枚举。类定义将
"prototype"
中所有方法的enumerable
标志设置为false
。这是好的,因为如果我们对一个对象执行
for..in
,我们通常不想要它的类方法。 -
类总是
use strict
。类构造中的所有代码都自动处于严格模式。
此外,class
语法带来了许多其他功能,我们将在后面探讨。
类表达式
就像函数一样,类可以在另一个表达式中定义、传递、返回、分配等。
以下是类表达式的示例
let User = class {
sayHi() {
alert("Hello");
}
};
类似于命名函数表达式,类表达式可以有名称。
如果类表达式有名称,则它仅在类内部可见
// "Named Class Expression"
// (no such term in the spec, but that's similar to Named Function Expression)
let User = class MyClass {
sayHi() {
alert(MyClass); // MyClass name is visible only inside the class
}
};
new User().sayHi(); // works, shows MyClass definition
alert(MyClass); // error, MyClass name isn't visible outside of the class
我们甚至可以动态地“按需”创建类,如下所示
function makeClass(phrase) {
// declare a class and return it
return class {
sayHi() {
alert(phrase);
}
};
}
// Create a new class
let User = makeClass("Hello");
new User().sayHi(); // Hello
getter/setter
就像字面量对象一样,类可以包含 getter/setter、计算属性等。
以下是使用 get/set
实现的 user.name
的示例
class User {
constructor(name) {
// invokes the setter
this.name = name;
}
get name() {
return this._name;
}
set name(value) {
if (value.length < 4) {
alert("Name is too short.");
return;
}
this._name = value;
}
}
let user = new User("John");
alert(user.name); // John
user = new User(""); // Name is too short.
从技术上讲,这种类声明通过在 User.prototype
中创建 getter 和 setter 来工作。
计算名称 […]
以下是使用方括号 [...]
计算方法名称的示例
class User {
['say' + 'Hi']() {
alert("Hello");
}
}
new User().sayHi();
这些特性很容易记住,因为它们类似于字面量对象。
类字段
类字段是最近添加到语言中的。
以前,我们的类只有方法。
“类字段”是一种允许添加任何属性的语法。
例如,让我们向 class User
添加 name
属性
class User {
name = "John";
sayHi() {
alert(`Hello, ${this.name}!`);
}
}
new User().sayHi(); // Hello, John!
因此,我们只需在声明中编写 "
类字段的重要区别在于它们设置在单个对象上,而不是 User.prototype
上
class User {
name = "John";
}
let user = new User();
alert(user.name); // John
alert(User.prototype.name); // undefined
我们还可以使用更复杂的表达式和函数调用来分配值
class User {
name = prompt("Name, please?", "John");
}
let user = new User();
alert(user.name); // John
使用类字段制作绑定方法
正如 函数绑定 章节中演示的那样,JavaScript 中的函数有一个动态 this
。它取决于调用的上下文。
因此,如果一个对象方法被传递并调用在另一个上下文中,this
将不再引用它的对象。
例如,此代码将显示 undefined
class Button {
constructor(value) {
this.value = value;
}
click() {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // undefined
这个问题称为“丢失 this
”。
有两种方法可以解决它,如 函数绑定 章节中所述
- 传递一个包装函数,例如
setTimeout(() => button.click(), 1000)
。 - 将方法绑定到对象,例如在构造函数中。
类字段提供了另一种非常优雅的语法
class Button {
constructor(value) {
this.value = value;
}
click = () => {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // hello
类字段 click = () => {...}
是逐对象创建的,每个 Button
对象都有一个单独的函数,其中 this
指向该对象。我们可以到处传递 button.click
,而 this
的值始终是正确的。
这在浏览器环境中特别有用,用于事件侦听器。
总结
基本类语法如下所示
class MyClass {
prop = value; // property
constructor(...) { // constructor
// ...
}
method(...) {} // method
get something(...) {} // getter method
set something(...) {} // setter method
[Symbol.iterator]() {} // method with computed name (symbol here)
// ...
}
MyClass
在技术上是一个函数(我们将其作为 constructor
提供),而方法、getter 和 setter 写入 MyClass.prototype
。
在接下来的章节中,我们将了解有关类的更多信息,包括继承和其他特性。
评论
<code>
标签,对于多行代码,请将它们包装在<pre>
标签中,对于 10 行以上的代码,请使用沙盒(plnkr、jsbin、codepen…)