类的声明

类在 ES6 中最简单的形式就是类声明,它看起来很像其他语言中的类。

基本的类声明

类声明以 class 关键字开始,其后是类的名称;剩余部分的语法看起来就像对象字面量中的方法简写,并且在方法之间不需要使用逗号。作为范例,此处有个简单的类声明:

class PersonClass {

    // equivalent of the PersonType constructor
    constructor(name) {
        this.name = name;
    }

    // equivalent of PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
}

let person = new PersonClass("Nicholas");
person.sayName();   // outputs "Nicholas"

console.log(person instanceof PersonClass);     // true
console.log(person instanceof Object);          // true

console.log(typeof PersonClass);                    // "function"
console.log(typeof PersonClass.prototype.sayName);  // "function"

这个 PersonClass 类声明的行为非常类似上个例子中的 PersonType。类声明允许你在其中使用特殊的 constructor 方法名称直接定义一个构造器,而不需要先定义一个函数再把它当作构造器使用。由于类的方法使用了简写语法,于是就不再需要使用 function 关键字。constructor 之外的方法名称则没有特别的含义,因此可以随你高兴自由添加方法。

自有属性(Own properties):该属性出现在实例上而不是原型上,只能在类的构造器或方法内部进行创建。在本例中,name 就是一个自有属性。我建议应在构造器函数内创建所有可能出现的自有属性,这样在类中声明变量就会被限制在单一位置(有助于代码检查)。

有趣的是,相对于已有的自定义类型声明方式来说,类声明仅仅是以它为基础的一个语法糖。PersonClass 声明实际上创建了一个拥有 constructor 方法及其行为的函数,这也是 typeof PersonClass 会得到 "function" 结果的原因。 此例中的 sayName() 方法最终也成为 PersonClass.prototype 上的一个方法,类似于上个例子中 sayName()PersonType.prototype 之间的关系。这些相似处允许你把自定义类型与类混合使用,而不必太担忧到底该用哪个。

为何要使用类的语法

尽管类与自定义类型之间有相似性,但仍然要记住一些重要的区别:

  1. 类声明不会被提升,这与函数定义不同。类声明的行为与 let 相似,因此在程序的执行到达声明处之前,类会存在于暂时性死区内。

  2. 类声明中的所有代码会自动运行在严格模式下,并且也无法退出严格模式。

  3. 类的所有方法都是不可枚举的,这是对于自定义类型的显著变化,后者必须用 Object.defineProperty() 才能将方法改变为不可枚举。

  4. 类的所有方法内部都没有 [[Construct]],因此使用 new 来调用它们会抛出错误。

  5. 调用类构造器时不使用 new,会抛出错误。

  6. 试图在类的方法内部重写类名,会抛出错误。

这样看来,上例中的 PersonClass 声明实际上就直接等价于以下未使用类语法的代码:

// direct equivalent of PersonClass
let PersonType2 = (function() {

    "use strict";

    const PersonType2 = function(name) {

        // make sure the function was called with new
        if (typeof new.target === "undefined") {
            throw new Error("Constructor must be called with new.");
        }

        this.name = name;
    }

    Object.defineProperty(PersonType2.prototype, "sayName", {
        value: function() {

            // make sure the method wasn't called with new
            if (typeof new.target !== "undefined") {
                throw new Error("Method cannot be called with new.");
            }

            console.log(this.name);
        },
        enumerable: false,
        writable: true,
        configurable: true
    });

    return PersonType2;
}());

首先要注意这里有两个 PersonType2 声明:一个在外部作用域的 let 声明,一个在 IIFE 内部的 const 声明。这就是为何类的方法不能对类名进行重写、而类外部的代码则被允许。构造器函数检查了 new.target,以保证被调用时使用了 new,否则就抛出错误。接下来,sayName() 方法被定义为不可枚举,并且此方法也检查了 new.target,它则要保证在被调用时没有使用 new。最后一步是将构造器函数返回出去。

此例说明了尽管不使用新语法也能实现类的任何特性,但类语法显著简化了所有功能的代码。

不变的类名

只有在类的内部,类名才被视为是使用 const 声明的。这意味着你可以在外部重写类名,但不能在类的方法内部这么做。例如:

class Foo {
    constructor() {
        Foo = "bar"; // 执行时抛出错误
    }
}
// 但在类声明之后没问题
Foo = "baz";

在此代码中,类构造器内部的 Foo 与在类外部的 Foo 是不同的绑定。内部的 Foo 就像是用 const 定义的,不能被重写,当构造器尝试使用任何值重写 Foo 时,都会抛出错误。但由于外部的 Foo 就像是用 let 声明的,你可以随时重写类名。