定义类


JavaScript 是基于原型的面向对象典范,然而,模拟类的需求一直都在,只是各自有不同的模拟方式与风格,这就造成了不同风格间要互动合作时的不便。

另一方面,虽然基于原型的 JavaScript 可以很有弹性地模拟不同风格的类,然而,有些特性的模拟有其困难,像是在继承时能透过 super 之类的方式调用父类方法等。

在基本的类模拟需求上,为了能提供一致的风格基础,也为了直接在语法上提供功能,以便能解决过去模拟类时遇到的一些困难,ES6 提供了类语法,若它的语法能解决需求就会建议采用,当然,若需要的类特性无法使用 ES6 类语法来实现,基于原型的方式仍然适用。

以〈模拟类的封装与继承〉中看到的例子来说:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.toString = function() {
    return '[' + this.name + ',' + this.age + ']';
};

var p = new Person('Justin', 35);
console.log(p.name); // Justin

在 ES6 中可以写为:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;     
    }

    toString() {
        return `[${this.name}, ${this.age}`;
    }
}

let p = new Person('Justin', 35);
console.log(p.name); // Justin

如果你熟悉其他语言中的类语法,看到这个 ES6 类,应该马上就能理解它的意义,本质上来说,上面的类语法很大的成份,可以视为前一个基于原因的范例之语法蜜糖。

对 ES6 类来说,Person本身是个Function实例,toString则是定义在Person.prototype上的一个特性,而Person.prototype.constructor参考的就是Person,这些都与 ES5 中对应的定义相同,如果透过Person.prototype添加特性,那么Person的实例也会找得到该特性;你也可以直接将toString参考的函数指定给某个变量,或者是指定为另一对象的特性,透过该对象来调用函数,该函数的this一样是依调用者而决定;每个透过new Person(...)构造出来的实例,本身的原型(__proto__)也都是参考至Person.prototype

然而不同的是,使用class定义的Person只能使用new来创建实例,直接使用Person(...)Person.call(...)Person.apply(...)都会发生TypeError,而Person也不会是定义在全局对象上的一个特性,另外,class定义的名称,就像let定义的名称那样,不会有 Hoist 的效果,因此在定义Person类之前,就尝试new Person(...),会发生ReferenceError

另一方面,class中定义的方法,虽然是等同于定义在prototype上,然而特性描述器上的enumerablefalse,因此for inObject.hasOwnPropertyObject.keys无法枚举,只能透过Object.getOwnPropertyNames,因为这个函数会一并获取可枚举与无法枚举的特性名称。

在类中,constructor定义了构造函数,如果类中没有编写constructor,也会自动加入一个无参数的constructor() {}constructor最后隐含地return this,如果在constructor明确地return某个对象,那么new的结果就会是该对象。

在 ES6 的类中也可以使用[]来定义方法,[]中可以是字符串、表达式的结果或者是Symbol,例如:

class Range {
    constructor(start, end) {
        this.start = start;
        this.end = end;
    }

    [Symbol.iterator]() {
        let i = this.start;
        let end = this.end;

        return {
            next() {
                return i < end ? 
                           {value: i++, done: false} :
                           {value: undefined, done: true}
            }           
        };
    }

    toString() {
        return `Range [${this.start}...${this.end - 1}]`;
    }   
}

let range = new Range(1, 4);
for(let i of range) {
    console.log(i);            // 显示 1 2 3
}
console.log(range.toString()); // 显示 Range [1...3]

你也可以在class中定义生成器函数:

class Range {
    constructor(start, end) {
        this.start = start;
        this.end = end;
    }

    *[Symbol.iterator]() {
        for(let i = this.start; i < this.end; i++) {
            yield i;
        }
    }

    toString() {
        return `Range [${this.start}...${this.end - 1}]`;
    }   
}

let range = new Range(1, 4);
for(let i of range) {
    console.log(i);               // 显示 1 2 3
}
console.log(range.toString());    // 显示 Range [1...3]

在 ES6 的类语法下,定义一个特性的 setter、getter 变得比较容易了,例如:

class Person {
    constructor(name, age) {
        this.__name__ = name;
        this.__age__ = age;     
    }

    toString() {
        return `[${this.__name__}, ${this.__age__}`;
    }

    get name() {
        return this.__name__;
    }

    get age() {
        return this.__age__;
    }
}

var p = new Person('Justin', 35);
console.log(p.name); // Justin

如果要定义 getter 的话,在方法前加上get,若是定义 setter 的话,在方法前使用set,上头模拟了对象的私有成员,在 ES6 类中,并没有定义私有成员的语法,你还是要以模拟的方式来定义。

在 ES6 的类中,若方法前加上static,那么该方法会是个静态方法,也就是以类名称为命名空间的一个函数:

class Foo {
    static orz() {

    }
}

与自行定义Foo函数,然而在函数上定义orz特性不同的是,在class上定义的静态方法,子类可以便于找到而使用,例如若有个class Foo2 extends Foo {},那么Foo2.orz()是可以调用的,ES6 中并没有定义静态特性的方式,因此仍只能使用Foo.CONT = 123这样的方式来模拟。

类也可以使用表达式的方式来创建,必要时也可以给予名称:

> let clz = class {
...     constructor(name) { this.name = name; }
... };
undefined
> new clz('xyz');
clz { name: 'xyz' }
> var clz2 = class Xyz {
...     constructor(name) { this.name = name; }
... };
undefined
> new clz2('xyz');
Xyz { name: 'xyz' }
>

在 ES6 中新增了new.target,如果函数或类的构造函数中有new.target,在使用new构造实例时,new.target代表了构造函数或类本身,否则就会是undefined,因此,在传统的构造函数定义时,可以如下检查,以达到强制使用new来构造对象的效果:

function Person(name, age) {
    if (new.target === Person) {
        this.name = name;
        this.age = age;
    } else {
        throw new TypeError(
           "Constructor Person cannot be invoked without 'new'");
    }   
}

在方法中,如果要递归调用,必须在方法前加上this,明确指定是调用自身方法,否则会尝试调用范围内可找到的函数,若找不到就是ReferenceError


展开阅读全文