ES5中的仿类结构
function PersonType(name){
this.name = name;
}
PersonType.prototype.sayName = function(){
console.log(this.name);
}
let person = new PersonType("Nicholas");
person.sayName(); // 输出 "Nicholas"
console.log(person instanceof PersonType); // true
console.log(person instanceof Object); // true
PersonType是一个构造器函数,sayName()方法被指派到原型上,因此PersonType的所有实例上都共享的此方法。
使用 new 运算符创建了 PersonType 的一个新实例 person ,此对象会被认为是一个通过原型继承了 PersonType 与 Object 的实例。
类的声明
基本类声明
类声明以class关键字开始,后面是类的名称。
在类内部的方法声明有点类似对象字面量中的方法简写,并且在方法之间并不需要使用逗号。
class PersonClass{
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}
let person = new PersonClass("nic");
person.sayName(); //"nic"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"
- 类声明允许你在其中使用特殊的constructor方法直接定义一个构造器
- 在构造器内部出现的属性都是实例属性,这种实例属性只能在类的构造器内部进行创建。
- sayName()则是成为的原型上的方法。
其实相对于ES5中的自定义类型声明方式来说,类声明仅仅是一个语法糖。PersonClass声明实际上创建了一个拥有constructor方法及一些行为的函数,因此typeof PersonClass
会得到function。此例中的 sayName() 方法最终也成为 PersonClass.prototype 上的一个方法。
类声明的特点
类声明与之前的自定义类型之间仍有一些重要的区别:
- 类声明不会被提升。类声明的行为与let类似,因此在程序的执行到达声明处之前,类会存在于暂时性死区内。
- 类声明中的所有代码会自动运行在严格模式下,并且也无法退出严格模式。
- 类的所有方法都是不可枚举的
- 类的所有方法内部都没有[[Construct]] ,因此使用 new 来调用它们会抛出错误。
- 调用类构造器时不使用new,会抛出错误
- 试图在类的方法内部重写类名,会抛出错误
类表达式
类与函数有相似之处,即它们都有两种形式,声明与表达式。
类表达式被设计用于变量声明,或可作为参数传递给函数。
基本的类表达式
let PersonClass = class{
constructor(name){
this.name = name;
}
sayName() {
console.log(this.name);
}
}
类表达式不需要在class关键字后使用标识符,除了语法差异,类表达式的功能等价于类声明。
使用类声明还是类表达式,主要是代码风格问题。相对于函数声明与函数表达式之间的区别,类声明与类表达式都不会被提升,因此对代码运行时的行为影响甚微。
具名类表达式
上一节是声明了一个匿名的类表达式,但是你也可以为类表达式命名:
let PersonClass = class PersonClass2{
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass2); // "undefined"
此处的类表达式被命名为PersonClass2,这个标识符只在类定义内部存在,因此只能用在类方法内部。在类的外部,typeof PersonClass2
的结果为 “undefined”,这是因为外部不存在PersonClass2绑定。
作为一级公民的类
能被当做值来使用的就是一级公民。这意味着:
- 能作为参数传给函数
- 能作为函数返回值
- 能给变量赋值
js中的函数就是一级公民,而ES6中的类同样也是一级公民。
可以作为参数传入函数:
function createObject(classDef) {
return new classDef();
}
let obj = createObject(class {
sayHi() {
console.log("Hi!");
}
})
obj.sayHi();
此处createObject()被调用时接受了一个匿名类表达式作为参数,使用new创建了该类的一个实例并将其返回。
类表达式的另一个用途是立即调用类构造器,以创建单例。
为此,必须要用new来配合类表达式,并在表达式后面添加括号:
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("nic");
person.sayName();
此处创建了一个匿名类表达式,并立即执行了它。类表达式后面的圆括号表示要调用前面的函数,并且还允许传入参数。
此模式允许你使用类语法来创建单例。
访问器属性
类还允许你在原型上定义访问器属性。
要创建getter,则要使用get关键字,并要与后方标识符之间留出空格,创建setter同理:
class Element{
constructor(element){
this.element = element;
}
get html(){
return this.element.innerHTML;
}
set html(value){
this.element.innerHTML = value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(Element.prototype,"html");
console.log("get" in descriptor); // true
console.log("set" in descriptor); // true
console.log(descriptor.enumerable); // false
此访问器属性被创建在Element.prototype上,并且像其他类属性一样被创建为不可枚举的属性。
需计算的成员名
与对象字面量类似,类方法与类访问器属性也都能使用需计算的名称,使用方括号包裹:
let methodName = "sayName";
class PersonType = {
constructor(name) {
this.name = name;
}
[methodName](){
console.log(this.name);
}
}
let me = new PersonClass("Nicholas");
me.sayName(); // "Nicholas"
这里的PersonClass使用了一个变量来命名类定义内的方法。
访问器属性也可以以相同的方式使用需计算名称:
let propertyName = "html";
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get [propertyName]() {
return this.element.innerHTML;
}
set [propertyName](value) {
this.element.innerHTML = value;
}
}
生成器方法
类内部允许定义生成器方法:
class MyClass {
*createIterator(){
yield 1;
yield 2;
yield 3;
}
}
let instance = new MyClass();
let iterator = instance.createIterator();
此代码创建了一个拥有createIterator()生成器的MyClass类,该方法返回了一个迭代器,它的值在生成器内部用硬编码提供。
可以使用Symbol.iterator来定义类的默认迭代器:
class Collection {
constructor(){
this.items = [];
}
*[Symbol.iterator](){
yield *this.item.values();
}
}
var collection = new Collection();
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for (let x of collection) {
console.log(x);
}
// 输出:
// 1
// 2
// 3
此处的生成器方法委托了数组的values()迭代器。现在Collection的任何实例都可以在for-of循环内部被直接使用。
静态成员
以上定义的访问器属性、生成器方法等,都是在原型上的,所有的实例都能访问。
若你想让方法与访问器属性只存在于类自身,那么就要使用静态成员。
在ES5中,一般通过直接在构造函数中添加额外方法来模拟静态成员:
function PersonType(name) {
this.name = name;
}
//静态方法
PersonType.create = function(name){
return new PersonType(name);
}
// 实例方法
PersonType.prototype.sayName = function() {
console.log(this.name);
};
var person = PersonType.create("Nicholas");
此处PersonType.create() 是一个静态方法,它不依赖于任何实例。
ES6简化了静态成员的创建,只要在方法和访问器属性的名称前添加static标注。
class PersonClass {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
//静态成员
static create(name){
return new PersonClass(name);
}
}
let person = PersonClass.create("Nicholas");
静态方法的定义只多了一个static关键字。
你可以在类中的任何方法与访问器属性上使用static关键字,唯一限制是不能用于constructor方法的定义。
静态成员不能用实例来访问,始终需要直接用类自身来访问它们。
继承
ES6之前,需要这样实现继承:
function Rectangle(length,width){
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
//子类
function Square(length){
Rectangle.call(this,length,length);
}
Square.prototype = Object.create(Rectangle.prototype);
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
Square 继承了 Rectangle ,为此它必须使用 Rectangle.prototype 所创建的一个新对象来重写 Square.prototype,并且还要在构造函数内部调用 Rectangle.call() 方法。
ES6类的出现让继承变得更容易:
- 使用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){
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
此次 Square 类使用了 extends 关键字继承了 Rectangle.
Square 构造器使用了 super() 配合指定参数调用了 Rectangle 的构造器。
继承了其他类的类被称为派生类:
- 如果派生类指定了构造器,则一定要使用super(),否则会抛出错误。
- 如果没有使用构造器,则super()方法会被自动调用,并会使用创建新实例时提供的所有参数:
class Square extends Rectangle {
// 没有构造器
}
// 等价于:
class Square extends Rectangle {
constructor(...args) {
super(...args);
}
}
使用super()时有几个需要注意的地方:
- 只能在派生类中使用super()。若尝试在非派生的类(即:没有使用 extends 关键字的类)或函数中使用它,就会抛出错误。
- 在构造器中,必须在访问this之前调用super()。 由于super()负责初始化this,因此试图先访问 this 自然就会造成错误。
- 唯一能避免调用super()的方法就是从构造器中返回一个对象。
屏蔽类方法
派生类中的方法总是会屏蔽基类的同名方法。
class Square extends Rectangle{
constructor(length){
super(length, length);
}
//重写并屏蔽 Rectangle.prototype.getArea()
getArea(){
return this.length * this.length;
}
}
此时Square的实例调用getArea()调用的就是Square.prototype上面的方法而不是Rectangle.prototype上面的方法。
当然,你总是可以使用super.getArea()来调用基类中的同名方法:
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
// 重写、屏蔽并调用了 Rectangle.prototype.getArea()
getArea() {
return super.getArea();
}
}
用这种方式使用 super ,其效果等同于在ES6对象中的 super 引用。this会被自动设置为正确的值。
继承静态成员
如果基类包含静态成员,那么这些静态成员在派生类中也是可用的。
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
static create(length, width) {
return new Rectangle(length, width);
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
var rect = Square.create(3,4);
console.log(rect instanceof Rectangle); // true
console.log(rect.getArea()); // 12
console.log(rect instanceof Square); // false
通过继承,派生类也以静态方法的方式调用:Square.create()
,并且其行为与Rectangle.create()
相同。
从表达式中派生类
你也可以从表达式中派生类,只要这个表达式能够返回一个具有[[Constructor]]属性以及原型的函数,就可以对其使用extends。
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
class Square extends Rectangle {
constructor(length){
super(length,length);
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x instanceof Rectangle); // true
Rectangle是ES5风格的构造器,Square则是ES6中的类,但由于Rectangle具有[[Constructor]]以及原型,因此Square可以继承他。
extends后面能接受任意类型的表达式,这带来了巨大可能性,例如动态地决定所要继承的类:
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
function getBase() {
return Rectangle;
}
class Square extends getBase(){
constructor(length) {
super(length, length);
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x instanceof Rectangle); // true
getBase() 函数作为类声明的一部分被直接调用,它返回了 Rectangle.
由于可以动态地决定基类,那也就能创建不同的继承方式:
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.length * this.width;
}
};
function mixin(...mixins) {
var base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}
class Square extends mixin(AreaMixin, SerializableMixin) {
constructor(length) {
super();
this.length = length;
this.width = length;
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x.serialize()); // "{"length":3,"width":3}"
此处使用了mixin而不是传统继承。mixin函数接受代表混入对象的任意数量的参数,他创建一个名为base的函数,并将每个混入对象的属性都赋值到新函数的原型上。此函数随后被返回,于是Square就能够使用extends关键字了。
Square的实例既有来自AreaMixin的的 getArea() 方法,又有来自SerializableMixin 的 serialize() 方法,这是通过原型继承实现的。
mixin() 函数使用了混入对象的所有自有属性,动态地填充了新函数的原型。
任意表达式都能在extends关键字后使用,但并非所有表达式的结果都是一个有效的类。特别地,下列表达式类型会导致错误:
- null
- 生成器函数
试图使用结果为上述值的表达式来创建一个新的类实例,都会抛出错误,因为不存在 [[Construct]] 可供调用。
继承内置对象
在 ES6 中的类,其设计目的之一就是允许从内置对象上进行继承。
在ES5的传统继承中,this的值会先被派生类创建,然后基类构造器(内置类型如Array)才被调用。这意味着一开始this就是派生类的实例,然后才使用了Array进行装饰,因此内置类型的一些内置行为是不能被成功继承到的。
在ES6基于类的继承中,this的值会先被基类创建,然后才被派生类的构造器修改。因此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 属性也能更新索引属性。
在类构造器中使用new.target
new.target属性允许你检测函数或构造方法是否通过是通过new运算符被调用的,如果是通过new调用的则返回一个指向函数或构造方法的引用,否则返回undefined。
你可以在类构造器中使用new.target,来判断类是如何被调用的。
在简单情况下,new.target就等于本类的构造器函数:
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
// new.target 就是 Rectangle
var obj = new Rectangle(3, 4); // 输出 true
此代码说明在 new Rectangle(3, 4) 被调用时, new.target 就等于 Rectangle 。
类构造器被调用时不能缺少new,因此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); // 输出 false
Square调用了Rectangle的构造器,因此当Rectangle构造器被调用时,new.target等于Square。
这很重要,因为构造器能根据如何被调用而有不同行为,并且这给了更改这种行为的能力。
可以利用new.target来创建一个抽象基类(不能被实例化的类):
class Shape {
constructor(){
if(new.target == Shape){
throw new Error("This class cannot be instantiated directly.")
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
var x = new Shape(); // 抛出错误
var y = new Rectangle(3, 4); // 没有错误
console.log(y instanceof Shape); // true
此例中的 Shape 类构造器会在 new.target 为 Shape 的时候抛出错误,意味着 new Shape() 永远都会抛出错误,即不能被实例化。
然而,你依然可以将 Shape 用作一个基类,正如 Rectangle 所做的那样。 super() 的调用执行了 Shape 构造器,而且 new.target 的值等于 Rectangle ,因此该构造器能够无错误地继续执行。
由于调用类时不能缺少 new ,于是 new.target 属性在类构造器内部就绝不会是 undefined 。