基本的类声明
类声明以 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 的一项新特性,它提供了更简洁的语法与更好的功能,通过安全一致的方式来
自定义一个对象类型。
网友评论