ES6之JS的类
基本的类声明
类声明以 class 关键字开始,其后是类的名称;剩余部分的语法看起来就像对象字面量中的
方法简写,并且在方法之间不需要使用逗号。
class PersonClass {
// 等价于 PersonType 构造器
constructor(name) {
this.name = name;
}
// 等价于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
}
let person = new PersonClass("Nicholas");
person.sayName(); // 输出 "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"
为何要使用类的语法?
类与自定义类型之间有相似性较高,但也要记住一些重要的区别:
- 类声明不会被提升,这与函数定义不同。类声明的行为与 let 相似,因此在程序的执行
到达声明处之前,类会存在于暂时性死区内。 - 类声明中的所有代码会自动运行在严格模式下,并且也无法退出严格模式。
- 类的所有方法都是不可枚举的,这是对于自定义类型的显著变化,后者必须用
Object.defineProperty() 才能将方法改变为不可枚举。 - 类的所有方法内部都没有 [[Construct]] ,因此使用 new 来调用它们会抛出错误。
- 调用类构造器时不使用 new ,会抛出错误。
- 试图在类的方法内部重写类名,会抛出错误。
类表达式
类与函数有相似之处,即它们都有两种形式:声明与表达式。函数声明与类声明都以适当的
关键词为起始(分别是 function 与 class ),随后是标识符(即函数名或类名)。函数具
有一种表达式形式,无须在 function 后面使用标识符;类似的,类也有不需要标识符的表
达式形式。类表达式被设计用于变量声明,或可作为参数传递给函数。
基本的类表达式
let PersonClass = class {
// 等价于 PersonType 构造器
constructor(name) {
this.name = name;
}
// 等价于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};
let person = new PersonClass("Nicholas");
person.sayName(); // 输出 "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"
类表达式不需要在 class 关键字后使用标识符。除了语法差异,类表达式的功能等价于类声明。
具名类表达式
在class 关键字后添加标识符,即具名类表达式
作为一级公民的类
在编程中,能被当作值来使用的就称为一级公民( first-class citizen ),意味着它能作为参
数传给函数、能作为函数返回值、能用来给变量赋值。
ES6 延续了传统,让类同样成为一级公民。这就使得类可以被多种方式所使用。例如,它能
作为参数传入函数:
function createObject(classDef) {
return new classDef();
}
let obj = createObject(class {
sayHi() {
console.log("Hi!");
}
});
obj.sayHi(); // "H
访问器属性
自有属性需要在类构造器中创建,而类还允许你在原型上定义访问器属性。为了创建一个
getter ,要使用 get 关键字,并要与后方标识符之间留出空格;创建 setter 用相同方式,只
是要换用 set 关键字。例如:
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, "html");
console.log("get" in descriptor); // true
console.log("set" in descriptor); // true
console.log(descriptor.enumerable); // false
需计算的成员名
对象字面量与类之间的相似点还不仅前面那些。类方法与类访问器属性也都能使用需计算的
名称。语法相同于对象字面量中的需计算名称:无须使用标识符,而是用方括号来包裹一个
表达式。例如:
let methodName = "sayName";
class PersonClass {
constructor(name) {
this.name = name;
}
[methodName]() {
console.log(this.name);
}
}
let me = new PersonClass("Nicholas");
me.sayName();
生成器方法
同函数生成器一样
静态成员
只要在方法与访问器属性的名称前添加正式的 static 标注。即构成类的静态方法
class PersonClass {
constructor(name) {
this.name = name;
}
// 等价于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
// 等价于 PersonType.create
static create(name) {
return new PersonClass(name);
}
}
let person = PersonClass.create("Nicholas");
静态成员不能用实例来访问,你始终需要直接用类自身来访问它们。
继承
使用派生类进行继承
使用extends 关键字来指定当前类所需要继承的函数即可实现类的继承。而如果想要访问继承基类的构造器则需要调用super()
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length) {
// 与 Rectangle.call(this, length, length) 相同
super(length, length);
}
}
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
继承了其他类的类被称为派生类( derived classes )。如果派生类指定了构造器,就需要
使用 super() ,否则会造成错误。若你选择不使用构造器, super() 方法会被自动调用,
并会使用创建新实例时提供的所有参数。
使用 super() 时需牢记以下几点:
- 你只能在派生类中使用 super() 。若尝试在非派生的类(即:没有使用 extends
关键字的类)或函数中使用它,就会抛出错误。- 在构造器中,你必须在访问 this 之前调用 super() 。由于 super() 负责初始化
this ,因此试图先访问 this 自然就会造成错误。- 唯一能避免调用 super() 的办法,是从类构造器中返回一个对象。
从表达式中派生类
在 ES6 中派生类的最强大能力,或许就是能够从表达式中派生类。只要一个表达式能够返回
一个具有 [[Construct]] 属性以及原型的函数,你就可以对其使用 extends 实现继承。
继承内置对象
在 ES6 基于类的继承中, this 的值会先被基类( Array )创建,随后才被派生类的构造
器( MyArray )所修改。结果是 this 初始就拥有作为基类的内置对象的所有功能,并能正
确接收与之关联的所有功能。
class MyArray extends Array {
// 空代码块
}
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
MyArray 直接继承了 Array ,因此工作方式与正规数组一致。与数值索引属性的互动更新
了 length 属性,而操纵 length 属性也能更新索引属性。这意味着你既能适当地继承
Array 来创建你自己的派生数组类,也同样能继承其他的内置对象。伴随着这些附加功能,
ES6 与派生类型有效解决了从内置类型进行派生这最后的特殊情况,不过这种情况仍然值得
继续探索。
Symbol.species 属性
继承内置对象一个有趣的方面是:任意能返回内置对象实例的方法,在派生类上却会自动返
回派生类的实例。因此,若你拥有一个继承了 Array 的派生类 MyArray ,诸如 slice() 之
类的方法都会返回 MyArray 的实例。例如:
class MyArray extends Array {
// 空代码块
}
let items = new MyArray(1, 2, 3, 4),
subitems = items.slice(1, 3);
console.log(items instanceof MyArray); // true
console.log(subitems instanceof MyArray); // true
在此代码中, slice() 方法返回了 MyArray 的一个实例。 slice() 方法是从 Array 上继
承的,原本应当返回 Array 的一个实例。而 Symbol.species 属性在后台造成了这种变化。
Symbol.species 知名符号被用于定义一个能返回函数的静态访问器属性。每当类实例的方法
(构造器除外)必须创建一个实例时,前面返回的函数就被用为新实例的构造器。下列内置
类型都定义了 Symbol.species :
- Array
- ArrayBuffer
- Map
- Promise
- RegExp
- Set
- 类型化数组
以上每个类型都拥有默认的 Symbol.species 属性,其返回值为 this ,意味着该属性总是
会返回自身的构造器函数。若你准备在一个自定义类上实现此功能,代码就像这样:
// 几个内置类型使用 species 的方式类似于此
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
在类构造器中使用 new.target
与判断函数是被如何被调用的一样。
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length)
}
}
// new.target 就是 Square
var obj = new Square(3); // 输出 fals
总结
ES6 的类让 JS 中的继承变得更简单,因此对于你已从其他语言学习到的类知识,你无须将其
丢弃。 ES6 的类起初是作为 ES5 传统继承模型的语法糖,但添加了许多特性来减少错误。
ES6 的类配合原型继承来工作,在类的原型上定义了非静态的方法,而静态的方法最终则被
绑定在类构造器自身上。类的所有方法初始都是不可枚举的,这更契合了内置对象的行为,
后者的方法默认情况下通常都不可枚举。此外,类构造器被调用时不能缺少 new ,确保了不
能意外地将类作为函数来调用。
基于类的继承允许你从另一个类、函数或表达式上派生新的类。这种能力意味着你可以调用
一个函数来判断需要继承的正确基类,也允许你使用混入或其他不同的组合模式来创建一个
新类。新的继承方式让继承内置对象(例如数组)也变为可能,并且其工作符合预期。
你可以在类构造器内部使用 new.target ,以便根据类如何被调用来做出不同的行为。最常
用的就是创建一个抽象基类,直接实例化它会抛出错误,但它仍然允许被其他类所继承。
总之,类是 JS 的一项新特性,它提供了更简洁的语法与更好的功能,通过安全一致的方式来
自定义一个对象类型。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。