ES6系列---类
大多数面向对象编程语言都支持类和类继承的特性,而JavaScript只能通过各种特定方式模仿并关联多个相似的对象。这个情况一直持续到ES5。由于类似的库层出不穷,最终ES6引入了类特性,统一了类和类继承的标准。
ES5模仿类
先看一段ES5中模仿类的代码:
function PersonType(name) { this.name = name; } PersonType.prototype.sayName = function(){ console.log(this.name); }; var person = new PersonType("Nicholas"); person.sayName(); console.log(person instanceof PersonType); // true console.log(person instanceof Object); // true
这段代码中的PersonType是一个构造函数,执行后创建一个名为name的属性;给PersonType的原型添加一个sayName()方法,所以PersonType对象的所有实例共享这个方法。然后使用new操作符创建一个PersonType的实例person,并最终证实了person对象确实是PersonType的实例。
ES6的类
ES6有一种与其他语言中类似的类特性:类声明。
类声明语法
class PersonType { // 等价于PersonType构造函数 constructor(name) { this.name = name; } // 等价于PersonType.prototype.sayName sayName() { console.log(this.name); } } let person = new PersonType("Nicholas"); person.sayName(); console.log(person instanceof PersonType); // true console.log(person instanceof Object); // true console.log(typeof PersonType); // "function" console.log(typeof PersonType.prototype.sayName); // "function"
通过类声明语法定义PersonType的行为与之前创建PersonType构造函数的过程相似,只是这里直接通过特殊的constructor方法名来定义构造函数。
访问器属性
尽管应该在类构造函数中创建自己的属性,但是类也支持直接在原型上定义访问器属性。创建getter时,需要在关键字get后紧跟一个空格和相应的标识符;创建setter时,只需把关键字get替换为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
这段代码中的CustomHTMLElement类是一个针对现有DOM元素的包装器,并通过getter和setter方法将这个元素的innerHTML方法委托给html属性,这个访问器属性是在CustomHTMLElement.prototype上创建的。
可计算成员名称
类和对象字面量还有更多相似之处,类方法和访问器属性也支持使用可计算名称:
let methodName = "sayName"; class PersonType { constructor(name) { this.name = name; } [methodName]() { console.log(this.name); } } let me = new PersonType("Nicholas"); me.sayName();
通过相同的方式可以在访问器属性中应用可计算名称:
let propertyName = "html"; class CustomHTMLElement { constructor(element) { this.element = element; } get [propertyName]() { return this.element.innerHTML; } set [propertyName](value) { this.element.innerHTML = value; } }
生成器方法
关于生成器和迭代器的知识点,可以参考ES6系列---生成器和迭代器。
在对象字面量中,可以通过在方法名前附加一个星号(*)的方式来定义生成器,在类中亦是如此:
class MyClass { *createIterator() { yield 1; yield 2; yield 3; } } let instance = new MyClass(); let iterator = instance.createIterator();
如果用对象来表示集合,又希望通过简单的方法迭代集合中的值,那么生成器方法就派上用场了。数组、Set集合及Map集合为开发者们提供了多个生成器方法来与集合中的元素交互。
尽管生成器方法很实用,但如果你的类是用来表示值的集合的,那么定义一个默认迭代器会更有用。通过Symbol.iterator定义生成器方法即可为类定义默认迭代器:
class Collection { constructor() { this.items = []; } *[Symbol.iterator]() { 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.log(x); } // 输出: // 1 // 2 // 3
静态成员
在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");
ES6简化了创建静态成员的过程,在方法或访问器属性名前使用正式的静态注释即可:
class PersonType { // 等价于PersonType构造函数 constructor(name) { this.name = name; } // 等价于PersonType.prototype.sayName sayName() { console.log(this.name); } // 等价于PersonType.create static create(name) { return new PersonType(name); } } let person = PersonType.create("Nicholas");
静态成员或方法,不可在实例中访问,必须要直接在类上访问。
继承与派生类
在ES6之前,实现继承与自定义类型是个不小的工作:
ES5中实现继承
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, { constructor: { value: Square, enumerable: true, writable: true, configurable: true } }); 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()方法即可访问基类的构造函数。下面是之前示例的ES6等价版:
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
这一次,Square类通过extends关键字继承Rectangle类,在Square构造函数中通过super()调用Rectangle构造函数并传入相应参数。
类方法重写
派生类中的方法总会覆盖基类中的同名方法:
class Square extends Rectangle { constructor(length) { super(length, length); } // 重写Rectangle.prototype.getArea()方法 getArea() { return this.length * this.length; } }
由于为Square定义了getArea()方法,便不能在Square实例中调用Rectangle.prototype.getArea()方法。当然,如果你想调用基类中的方法,则可以调用super.getArea()方法,就像这样:
class Square extends Rectangle { constructor(length) { super(length, length); } // 重写后调用Rectangle.prototype.getArea() getArea() { return super.getArea(); } }
静态成员继承
如果基类有静态成员,那么这些静态成员在派生类中也可用:
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) { // 等价于Rectangle.call(this, length, 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
在这段代码中,新的静态方法create()被添加到Rectangle类中,继承后的Square.create()与Rectangle.create()行为一致。
派生自表达式的类
ES6最强大的一面或许是从表达式导出类的功能了。只要表达式可以被解析为一个函数并且具有[[Construct]]属性和原型,那么就可以用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是一个类,由于Rectangle具有[[Construct]]属性和原型,因此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 Squre 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,此示例实现的功能与之前的示例等价。由于可以动态确定使用哪个基类,因而可以创建不同的继承方法。例如,可以这样创建mixin:
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,再将每一个mixin对象的属性值赋值给base的原型,最后mixin函数返回这个base函数,所以Square类就可以基于这个返回的函数用extends进行扩展。
Square的实例拥有来自AreaMixin对象的getArea()方法和来自SerializableMixin对象的serialize方法,这都是通过原型继承实现的,mixin()函数会用所有mixin对象的自有属性动态填充新函数的原型。
类的构造函数中使用new.target
在类的构造函数中也可以通过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。
继承情况下,有所不同:
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的构造函数,所以当调用发生时new.target等于Square。据此,我们可以创建一个抽象基类(不能被实例化的类),就像这样:
// 抽象基类 class Shape { constructor() { if (new.target === Shape) { throw new Error("这个类不能被直接实例化。"); } } } 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
在这个示例中,每当new.target是Shape时构造函数总会抛出错误,这相当于调用new Shape()时总会出错。但是,仍可用Shape作为基类派生其他类。
相关推荐
是一道经常出现在前端面试时的问题。如果只是简单的了解new关键字是实例化构造函数获取对象,是万万不能够的。更深入的层级发生了什么呢?同时面试官想从这道题里面考察什么呢?下面胡哥为各位小伙伴一一来解密。