Class 基本语法
“class” 语法
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}
// 用法:
let user = new User("John");
user.sayHi();
当 new User(“John”) 被调用:
- 一个新对象被创建。
- constructor 使用给定的参数运行,并为其分配 this.name。
什么是 class?
在 JavaScript 中,类是一种函数。
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// 佐证:User 是一个函数
alert(typeof User); // function
Class 字段
例如,让我们在 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
使用类字段制作绑定方法
正如 函数绑定 一章中所讲的,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)。
- 将方法绑定到对象,例如在 constructor 中。
类字段提供了另一种非常优雅的语法:
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 的值总是正确的。
类继承
“extends” 关键字
在内部,关键字 extends 使用了很好的旧的原型机制进行工作。它将 Rabbit.prototype.[[Prototype]] 设置为 Animal.prototype。
所以,如果在 Rabbit.prototype 中找不到一个方法,JavaScript 就会从 Animal.prototype 中获取该方法。
让我们创建一个继承自 Animal 的 class Rabbit:
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!
在 extends 后允许任意表达式
function f(phrase) {
return class {
sayHi() { alert(phrase); }
};
}
class User extends f("Hello") {}
new User().sayHi(); // Hello
这里 class User 继承自 f(“Hello”) 的结果。
重写方法
但是通常来说,我们不希望完全替换父类的方法,而是希望在父类方法的基础上进行调整或扩展其功能。我们在我们的方法中做一些事儿,但是在它之前或之后或在过程中会调用父类方法。
Class 为此提供了 “super” 关键字。
- 执行 super.method(…) 来调用一个父类方法。
- 执行 super(…) 来调用一个父类 constructor(只能在我们的 constructor 中)。
例如,让我们的 rabbit 在停下来的时候自动 hide:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stands still.`);
}
}
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
stop() {
super.stop(); // 调用父类的 stop
this.hide(); // 然后 hide
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run(5); // White Rabbit 以速度 5 奔跑
rabbit.stop(); // White Rabbit 停止了。White rabbit hide 了!
箭头函数没有 super
如果被访问,它会从外部函数获取。例如:
class Rabbit extends Animal {
stop() {
setTimeout(() => super.stop(), 1000); // 1 秒后调用父类的 stop
}
}
重写 constructor
根据 规范,如果一个类扩展了另一个类并且没有 constructor,那么将生成下面这样的“空” constructor:
class Rabbit extends Animal {
// 为没有自己的 constructor 的扩展类生成的
constructor(...args) {
super(...args);
}
}
继承类的 constructor 必须调用 super(…),并且 (!) 一定要在使用 this 之前调用。
在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的。派生构造器具有特殊的内部属性 [[ConstructorKind]]:”derived”。这是一个特殊的内部标签。
为了让 Rabbit 的 constructor 可以工作,它需要在使用 this 之前调用 super(),就像下面这样
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
super(name); // 需要在使用 this 之前调用 super()
this.earLength = earLength;
}
// ...
}
// 现在可以了
let rabbit = new Rabbit("White Rabbit", 10);
alert(rabbit.name); // White Rabbit
alert(rabbit.earLength); // 10
重写类字段: 一个棘手的注意要点
我们不仅可以重写方法,还可以重写类字段。
class Animal {
name = 'animal';
constructor() {
alert(this.name); // (*)
}
}
class Rabbit extends Animal {
name = 'rabbit';
}
new Animal(); // animal
new Rabbit(); // animal
因为 Rabbit 中没有自己的构造器,所以 Animal 的构造器被调用了。有趣的是在这两种情况下:new Animal() 和 new Rabbit(),在 () 行的 alert 都打印了 animal。
*换句话说, 父类构造器总是会使用它自己字段的值,而不是被重写的那一个。
再看看 重写方法的例子:
class Animal {
showName() { // 而不是 this.name = 'animal'
alert('animal');
}
constructor() {
this.showName(); // 而不是 alert(this.name);
}
}
class Rabbit extends Animal {
showName() {
alert('rabbit');
}
}
new Animal(); // animal
new Rabbit(); // rabbit
当父类构造器在派生的类中被调用时,它会使用被重写的方法。
……但对于类字段并非如此。
这里为什么会有这样的区别呢?
实际上,原因在于字段初始化的顺序。类字段是这样初始化的:
- 对于基类(还未继承任何东西的那种),在构造函数调用前初始化。
-
深入:内部探究和 [[HomeObject]]
静态属性和静态方法
私有的和受保护的属性和方法
内部接口和外部接口
就面向对象编程(OOP)而言,内部接口与外部接口的划分被称为 封装)。
在面向对象的编程中,属性和方法分为两组: 内部接口 —— 可以通过该类的其他方法访问,但不能从外部访问的方法和属性。
- 外部接口 —— 也可以从类的外部访问的方法和属性。
在 JavaScript 中,有两种类型的对象字段(属性和方法):
- 公共的:可从任何地方访问。它们构成了外部接口。到目前为止,我们只使用了公共的属性和方法。
- 私有的:只能从类的内部访问。这些用于内部接口。
受保护的 “waterAmount”
受保护的属性通常以下划线 _ 作为前缀。这不是在语言级别强制实施的,但是程序员之间有一个众所周知的约定,即不应该从外部访问此类型的属性和方法。
所以我们的属性将被命名为 _waterAmount只读的 “power”
只需要设置 getter,而不设置 setter
受保护的字段是可以被继承的私有的 “#waterLimit”
This is a recent addition to the language. Not supported in JavaScript engines, or supported partially yet, requires polyfilling.
私有属性和方法应该以 # 开头。它们只在类的内部可被访问。
私有字段不能通过 this[name] 访问扩展内建类
内建的类,例如 Array,Map 等也都是可以扩展的(extendable)。类检查:”instanceof”
instanceof 操作符用于检查一个对象是否属于某个特定的 class。同时,它还考虑了继承。instanceof 操作符
如果 obj 隶属于 Class 类(或 Class 类的衍生类),则返回 true。
```javascript class Rabbit {} let rabbit = new Rabbit();obj instanceof Class
// rabbit 是 Rabbit class 的对象吗? alert( rabbit instanceof Rabbit ); // true
通常,instanceof 在检查中会将原型链考虑在内。此外,我们还可以在静态方法 Symbol.hasInstance 中设置自定义逻辑。
<a name="RQ1Ec"></a>
## 福利:使用 Object.prototype.toString 方法来揭示类型
内建的 toString 方法可以被从对象中提取出来,并在任何其他值的上下文中执行。其结果取决于该值。
- 对于 number 类型,结果是 [object Number]
- 对于 boolean 类型,结果是 [object Boolean]
- 对于 null:[object Null]
- 对于 undefined:[object Undefined]
- 对于数组:[object Array]
- ……等(可自定义)
{}.toString 是一种“更高级的” typeof
```javascript
let s = Object.prototype.toString;
alert( s.call(123) ); // [object Number]
alert( s.call(null) ); // [object Null]
alert( s.call(alert) ); // [object Function]
Symbol.toStringTag
Mixin 模式
在 JavaScript 中,我们只能继承单个对象。每个对象只能有一个 [[Prototype]]。并且每个类只可以扩展另外一个类。
换句话说,mixin 提供了实现特定行为的方法,但是我们不单独使用它,而是使用它来将这些行为添加到其他类中。
一个 Mixin 实例
在 JavaScript 中构造一个 mixin 最简单的方式就是构造一个拥有实用方法的对象,以便我们可以轻松地将这些实用的方法合并到任何类的原型中。
// mixin
let sayHiMixin = {
sayHi() {
alert(`Hello ${this.name}`);
},
sayBye() {
alert(`Bye ${this.name}`);
}
};
// 用法:
class User {
constructor(name) {
this.name = name;
}
}
// 拷贝方法
Object.assign(User.prototype, sayHiMixin); // 这里没有继承,只有一个简单的方法拷贝。
// 现在 User 可以打招呼了
new User("Dude").sayHi(); // Hello Dude!
所以 User 可以从另一个类继承,还可以包括 mixin 来 “mix-in“ 其它方法,就像这样:
class User extends Person {
// ...
}
Object.assign(User.prototype, sayHiMixin);