第九章 JS的类
《深入理解ES6》—— Nicholas C. Zakas
JS从创建之初就不支持类,
也没有把类继承作为定义相似对象以及关联对象的主要方式。
很多库都创建了一些工具,让JS显得貌似能支持类,导致ES6最终引入了类。
ES6的类并不与其他语言的类完全相同,
所具备的独特性正配合了JS的动态本质。
1. ES5中的仿类结构
ES6之前不存在类,但与类最接近的是:
创建一个构造器,然后将方法指派到该构造器的原型上。
这种方式通常被称为创建一个自定义类型,如:
function PersonType( name ) {
this.name = name;
}
PersonType.prototype.sayName = function () {
console.info( this.name );
}
let person = new PersonType( "吴钦飞" );
person.sayName(); //=> 吴钦飞
person instanceof PersonType; //=> true
person instanceof Object; //=> true
使用 new
运算符创建了 PersonType
的一个新实例 person
,
此对象会被认为是一个通过原型继承了 PersonType
与 Object
的实例。
这种基本模式在许多对类进行模拟的JS库中都存在,而这也是ES6类的出发点。
2. 类的声明
2.1. 基本的类声明
格式:class 类名 对象字面量的简写形式。
class PersonClass {
// <==> PersonType的构造器
constructor( name ) {
this.name = name;
}
// <==> PersonType.prototype.sayName
sayName() {
console.info( this.name );
}
}
let person = new PersonClass( "吴钦飞" );
person.sayName();
typeof PersonClass; //=> "function"
PersonClass
实际上创建了一个拥有 constructor
方法及其行为的函数。
自有属性:own properties,该属性出现在实例上而不是原型上,
只能在类的构造器或方法内部进行创建。
建议在构造器函数内创建所有可能出现的自有属性,
这样在类中声明变量就会被限制在单一位置(有助于代码检查)。
相对于已有的自定义类型声明方式来说,
类声明仅仅是以它为基础的一个语法糖。
2.2. 为何要使用类的语法
类与自定义类型的区别:
- 类声明不会被提升。在程序的执行到达声明处之前,类都存在暂时性死区。
- 类声明中的所有代码都自动运行在严格模式下
- 类的所有方法都不可枚举
- 类的所有方法内部都没有
[[Construct]]
,因此使用new
调用会报错 - 调用类构造器时不使用
new
,会抛异常。 - 视图在类的方法内部重写类名,会抛出错误.
等价于一下未使用类语法的代码:
let PersonType = (function() {
"use strict";
const PersonType = function( name ) {
if ( typeof new.target === "undefined" ) {
throw new Error( "Constructor must be called with new." );
}
this.name = name;
};
Object.defineProperty( PersonType.prototype, "sayName", {
value: function () {
if ( typeof new.target !== "undefined" ) {
throw new Error("Method cannot be called with new.");
}
console.info( this.name );
},
enumerable: false,
writable: true,
configurable: true
} );
return PersonType;
})();
上例说明了尽管不使用新语法也能实现类的任何特性,但类语法显著简化了所有功能的代码。
3. 类表达式
类和函数都有两种形式:声明和表达式。
类表达式被设计用于变量声明、或可作为参数传递给函数。
3.1. 基本的类表达式
let Person = class {
constructor( name ) {
this.name = name;
}
sayName() {
console.info( this.name );
}
};
3.2. 具名类表达式
let Person = class Person2 {
constructor( name ) {
this.name = name;
}
sayName() {
console.info( this.name );
}
};
typeof Person2; //=> "undefined"
Person2
只能在类的内部使用,在外部使用会报语法错误。
4. 作为一级公民的类
在编程中,能被当做值来使用的就称为** 一级公民(first-class citizen)**。
它能 作为参数传递给函数、作为函数的返回值、能给变量赋值。
JS的函数就是一级公民(又称为一级函数)。
ES6的类也是一级公民。
类作为参数传入函数:
function createObject( ClassDef ) {
return new ClassDef();
}
let obj = createObject( class {
sayHi() {
console.info( "Hi" );
}
} );
obj.sayHi();
创建单例:
let person = new class{
constructor( name ) {
this.name = name;
}
sayName() {
console.info( this.name );
}
}( "吴钦飞" );
person.sayName();
5. 访问器属性
在类构造器中可以创建 自有属性,
可以使用关键字set
/get
在原型上定义 访问器属性。
class CustomHTMLElement {
constructor( element ) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(content) {
this.element.innerHTML = content;
}
}
const div = document.createElement('div');
const customDiv = new CustomHTMLElement(div);
customDiv.html = 123;
customDiv.html; //=> "123"
CustomHTMLElement
类用于包装DOM元素,
它的html
属性拥有getter与setter,委托了元素自身的 innerHTML
方法,
它创建在原型上,并且不可枚举。
等价非类代码实现 需要写大量代码:
let CustomHTMLElement = ( function() {
"use strict";
const CustomHTMLElement = function( element ) {
if ( typeof new.target === "undefined" ) {
throw new Error( "构造器必须通过 new 来调用" );
}
this.element = element;
};
Object.defineProperty( CustomHTMLElement.prototype, "html", {
enumerable: false,
configurable: true,
get: function() {
return this.element.innerHTML;
},
set: function( value ) {
this.element.innerHTML = value;
}
} );
return CustomHTMLElement;
} )();
6. 需计算的成员名
类方法、类访问器的名称可通过 [表达式]
计算获得。
let methodName = "sayName";
let propertyName = "html";
class PersonClass {
constructor( name ) {
this.name = name;
}
[methodName]() {
console.info( this.name );
}
get [propertyName]() {}
set [propertyName]() {}
}
new PersonClass( "吴钦飞" ).sayName();
7. 生成器方法
在表示集合的自定义类中定义一个默认迭代器。
class Collection {
constructor() {
this.items = [];
}
*[Symbol.iterator]() {
// 委托到该数组的values()迭代器
yield *this.items.values();
}
}
var collection = new Collection();
collection.items.push( 1 );
collection.items.push( 2 );
collection.items.push( 3 );
for ( let x of collection ) {
console.info( x );
}
8. 静态成员
ES6之前,直接在构造器上添加额外的方法来模拟静态成员。
ES6的类简化了静态成员的创建,
只要在方法与访问器属性的名称前加上 static
关键字即可。
class PersonClass {
constructor( name ) {
this.name = name;
}
sayName() {
console.info( this.name );
}
static create( name ) {
return new PersonClass( name );
}
}
PersonClass.create( "吴钦飞" ).sayName();
9. 继承
9.1. 基础
ES6之前的继承:
function Rectangle( width, height ) {
this.width = width;
this.height = height;
}
Rectangle.prototype.getArea = function () {
return this.width * this.height;
};
function Square( width ) {
Rectangle.call( this, width, width );
}
Square.prototype = Object.create( Rectangle.prototype, {
constructor: {
value: Square,
enumerable: true,
writable: true,
configurable: true
}
} );
var square = new Square( 3 );
console.info( square.getArea() ); //=> 9
console.info( square instanceof Square ); //=> true
console.info( square instanceof Rectangle );//=> true
Square
继承 Rectangle
:
- 重写
Square.prototype
,等于Rectangle.prototype
创建的对象 - 在构造函数里先调用
Rectangle
的构造函数进行初始化this
ES6类的继承:
class Rectangle {
constructor( width, height ) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(width) {
super( width, width );
}
}
var square = new Square( 3 );
console.info( square.getArea() ); //=> 9
console.info( square instanceof Square ); //=> true
console.info( square instanceof Rectangle );//=> true
Square
继承 Rectangle
:
-
使用
extends
关键字 -
使用
super()
初始化this
-
必须在访问 this 之前调用
super()
- 如果派生类指定了构造器,则需要显式使用
super()
- 如果派生类未指定构造器,
super()
会自动调用
class Square extends Rectangle { // 未指定构造器 } // 等价于:(添加默认构造器) class Square extends Rectangle { constructor( ...args ) { super( ...args ); } }
- 如果派生类指定了构造器,则需要显式使用
9.2. 屏蔽类方法
派生类中方法会屏蔽掉基类的同名方法,
可使用 super.methodA()
访问基类的方法。
9.3. 继承静态成员
派生类可继承基类的静态成员。
9.4. 从表达式中派生类
class ClassName extends 表达式 {
}
该表达式返回一个具有 [[Construct]]
属性的函数。
9.5. 继承内置对象
ES6中的类,允许从内置对象上进行继承。
class MyArray extends Array {
}
ES5中传统的继承,this
值先被派生类(MyArray
)创建,
随后调用基类构造器(Array.apply()
)。
这意味着 this
一开始就是 MyArray
的实例,
之后才使用了 Array
的附加属性对其进行了装饰。
ES6基于类的继承中,this
的值会先被基类(Array
)创建,
随后才被派生类的构造器(MyArray
)所修改。
结果是this
初始就拥有作为基类的内置对象的所有功能,
并能正确接收与之关联的所有功能。
9.6. Symbol.species 属性
继承内置对象的特点:任意能返回内置对象实例的方法,在派生类上却自动返回派生类的实例。
如,调用 MyArray
实例的 slice()
方法会返回 MyArray
的实例。
是 Symbol.species
属性在后台造成了这种变化。
10. 在类构造器中使用 new.target
当被派生类调用构造器时,new.target
不等于基类。
这可以创建不可实例化的基类。
class Shape {
constructor() {
if ( new.target === Shape ) {
throw new Error( "基类不能实例化" );
}
}
}