ES6学习笔记:类

xiaoxiao2021-02-28  131

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 。

转载请注明原文地址: https://www.6miu.com/read-22206.html

最新回复(0)