ES6学习笔记:类

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 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值