在 ECMASCript 6 之前,应用构造函数模式与原型模式以及它们的组合来模仿类的行为 。然而这几种策略都有本人的问题,也有相应的斗争。而实现继承也会显得十分简短和凌乱。因而,ECMASCript5 新引入了 class 关键字来定义类,但实际上背地应用的依然是原型和构造函数的概念。

类定义

类是 “非凡的函数”,因而定义类也有两种形式。第一种定义类的形式是申明类。

class Person {}

另一种定义类的形式是类表达式。

const Person = class {};

类表达式的名称是可选的。在把类表达式赋值给变量后,能够通过 name 属性获得类表达式的名称字符串。但不能在类表达式作用域内部拜访这个标识符。

let Person = class PersonName {    identify() {        console.log(Person.name, PersonName.name);    }}let p = new Person();p.identify(); // PersonName PersonNameconsole.log(Person.name); // PersonNameconsole.log(PersonName); // ReferenceError: PersonName is not defined

二者之间有重要区别。函数申明能够晋升,类申明不会晋升。

let p = new Person();    // ReferenceError: Cannot access 'Person' before initializationclass Person {}let s = new Student();    // ReferenceError: Cannot access 'Student' before initializationlet Student = class {};

类能够蕴含构造函数办法、实例办法、获取函数、设置函数和动态类办法,但这些都不是必须的。空的类定义照样无效。默认状况下,类定义中的代码都在严格模式下执行。

class Person {    // 构造函数    constructor() {}    // 获取函数    get name() {}    // 静态方法    static of() {}}

构造方法

constructor 办法是类定义中的构造方法。当类创立实例时,会调用这个办法。构造方法的定义不是必须的,不定义构造方法相当于将结构函办法定义为空函数。

当在类中定义了 constructor 办法时,应用 new 实例对象时,会调用 constructor 办法进行实例化,并且执行如下操作。

  1. 在内存中创立一个新对象。
  2. 这个新对象外部的 [[Prototype]] 指针被赋值为构造函数的 prototype 属性。
  3. 构造函数外部的 this 指向新对象。
  4. 执行构造函数外部的代码。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创立的新对象。
class Person {    constructor(name) {        console.log(arguments.length);        this.name = name || null;    }}class Student {    constructor() {        this.name = "default name";    }}

类实例化时传入的参数会用作构造函数的参数。如果不须要参数,则类名前面的括号也是可选的。

let p1 = new Person;    // 0let p2 = new Person();   // 0let p3 = new Person("小刚");    // 1console.log(p1.name);    // nullconsole.log(p2.name);    // nullconsole.log(p3.name);    // 小刚let s2 = new Student();console.log(s2.name);    // default name

默认状况下,类构造函数会在执行之后返回 this 对象。构造函数返回的对象会被用作实例化的对象,如果没有什么援用新创建的 this 对象,那么这个对象会被销毁。不过,如果返回的不是this 对象,而是其余对象,那么这个对象不会通过 instanceof 操作符检测出跟类有关联,因为这个对象的原型指针并没有被批改。

class Person {    constructor(override) {        this.name = '小齐';        if (override) {            return {                nickname: "昵称"            };        }    }}let p1 = new Person(),    p2 = new Person(true);console.log(p1); // Person { name: '小齐' }console.log(p1 instanceof Person); // trueconsole.log(p2); // { nickname: '昵称' }console.log(p2 instanceof Person); // false

类构造函数与构造函数的次要区别是,调用类构造函数必须应用 new 操作符。而一般构造函数如果不应用 new 调用,那么就会以全局的 this(通常是 window)作为外部对象。调用类构造函数时如果忘了应用 new 则会抛出谬误:

function Person() {}class Student {}// 把window 作为this 来构建实例let p = Person();let a = Student();    // TypeError: Class constructor Student cannot be invoked without 'new'

类构造函数没有什么非凡之处,实例化之后,它会成为一般的实例办法(但作为类构造函数,依然要应用 new 调用)。因而,实例化之后能够在实例上援用它:

class Person {}// 应用类创立一个新实例let p1 = new Person();p1.constructor();    // TypeError: Class constructor Person cannot be invoked without 'new'// 应用对类构造函数的援用创立一个新实例let p2 = new p1.constructor();    // 这里能够通过

在 ECMAScript 中,应用 class 定义的类,通过 typeof 来检测,其实质是一个函数:

class Person {}console.log(Person);    // [class Person]console.log(typeof Person);    // function

类标识符有 prototype 属性,而这个原型也有一个 constructor 属性指向类本身:

class Person{}console.log(Person.prototype); // Person {}console.log(Person === Person.prototype.constructor); // true

与一般构造函数一样,能够应用 instanceof 操作符查看构造函数原型是否存在于实例的原型链中:

class Person {}let p = new Person();console.log(p instanceof Person); // true

类是 JavaScript 的一等公民,因而能够像其余对象或函数援用一样把类作为参数传递:

// 类能够像函数一样在任何中央定义,比方在数组中let classes = [    class {        constructor(id) {            this.id = id;            console.log(`instance ${this.id}`);        }    }];function tryInstance(classDefinition, id) {    return new classDefinition(id);}let instance = tryInstance(classes[0], 3.14); // instance 3.14

与立刻调用函数表达式类似,类也能够立刻实例化:

// 因为是一个类表达式,所以类名是可选的let p = new class Person {    constructor(x) {        console.log(x);    }}('小强');    // 小强 console.log(p); // Person {}

实例办法

每次创立实例时,都会执行类构造方法。而在类的构造方法中,通过 this 能够为该类增加 实例属性。每个实例都对应一个惟一的成员对象,这意味着所有成员都不会在原型上共享:

class Person {    constructor() {        // 这个例子先应用对象包装类型定义一个字符串        // 为的是在上面测试两个对象的相等性        this.name = new String("小丽");        this.sayName = () => console.log(this.name);        this.nicknames = ['大节', '小点']    }}let p1 = new Person(),    p2 = new Person();p1.sayName(); // 小丽p2.sayName(); // 小丽console.log(p1.name === p2.name); // falseconsole.log(p1.sayName === p2.sayName); // falseconsole.log(p1.nicknames === p2.nicknames); // falsep1.name = p1.nicknames[0];p2.name = p2.nicknames[1];p1.sayName(); // 大节p2.sayName(); // 小点

动态属性或原型的数据属性必须定义在类定义的里面。

class Person {    sayName() {        console.log(`${Person.greeting} ${this.name}`);    }}// 在类上定义数据成员Person.greeting = 'My name is';// 在原型上定义数据成员Person.prototype.name = '柯林';let p = new Person();p.sayName();    // My name is 柯林

留神:类定义中之所以没有显示反对增加数据成员,是因为在共享指标上增加可变数据成员是一种反模式。一般来说,对象实例应该单独领有通过 this 援用的数据。

原型办法

为了在实例间共享方法,类定义语法把在类块中定义的办法作为原型办法。

class Person {    constructor() {        // 增加到this 的所有内容都会存在于不同的实例上        this.locate = () => console.log('instance');    }    // 在类块中定义的所有内容都会定义在类的原型上    locate() {        console.log('prototype');    }}let p = new Person();p.locate(); // instancePerson.prototype.locate(); // prototype

静态方法

能够在类上能够应用 staitc 关键字定义静态方法。调用静态方法不须要实例化该类,但不能通过一个类实例调用静态方法。在静态方法中,this 援用类本身。

class Person {    constructor() {        // 增加到this 的所有内容都会存在于不同的实例上        this.locate = () => console.log('instance', this);    }    // 定义在类的原型对象上    locate() {        console.log('prototype', this);    }    // 定义在类自身上    static locate() {        console.log('class', this);    }}let p = new Person();p.locate(); // instance Person { locate: [Function] }Person.prototype.locate(); // prototype Person {}Person.locate(); // class [class Person]

迭代器与生成器

类定义语法反对在原型和类自身上定义生成器办法:

class Person {    // 在原型上定义生成器办法    * createNicknameIterator() {        yield '杰克狗';        yield '杰克鼠';        yield '杰克猫';    }    // 在类上定义生成器办法    static* createJobIterator() {        yield '邦德一';        yield '邦德二';        yield '邦德三';    }}let jobIter = Person.createJobIterator();console.log(jobIter.next().value); // 邦德一console.log(jobIter.next().value); // 邦德二console.log(jobIter.next().value); // 邦德三let p = new Person();let nicknameIter = p.createNicknameIterator();console.log(nicknameIter.next().value); // 杰克狗console.log(nicknameIter.next().value); // 杰克鼠console.log(nicknameIter.next().value); // 杰克猫

因为反对生成器办法,所以能够通过增加一个默认的迭代器,把类实例变成可迭代对象:

class Person {    constructor() {        this.nicknames = ['杰克狗', '杰克鼠', '杰克猫'];    }    *[Symbol.iterator]() {        yield *this.nicknames.entries();    }}let p = new Person();for (let [idx, nickname] of p) {    console.log(nickname);}// 杰克狗// 杰克鼠// 杰克猫

也能够只返回迭代器实例:

class Person {    constructor() {        this.nicknames = ['杰克狗', '杰克鼠', '杰克猫'];    }    [Symbol.iterator]() {        return this.nicknames.entries();    }}let p = new Person();for (let [idx, nickname] of p) {    console.log(nickname);}// 杰克狗// 杰克鼠// 杰克猫

继承

ECMAScript 6 通过 extends 关键字提供的语法糖来与任何领有 [[Construct]] 和原型的对象实现的类继承机制。

class Person {}class Student extends Person{}let s = new Student();console.log(s instanceof Student);    // trueconsole.log(s instanceof Person);    // true

然而,这种继承形式也能够继承一般的构造函数。

function Person() {}class Student extends Person{}let s = new Student();console.log(s instanceof Student);    // trueconsole.log(s instanceof Person);    // true

这种继承形式能够用在类表达式上。

let Student = class extends Person {}

子类还能够通过 super 关键字援用它们的原型,而且只能在子类中应用。子类中定义了构造方法,必须调用 super() 之后,能力应用 this。这里调用 super() 会调用父类的构造方法,并将返回的实例赋值给 this

class Person {    constructor(name) {        this.name = name;    }}class Student extends Person{    constructor(name) {        super(name);        console.log(this);    }}let s = new Student("小王");    // Student { name: '小王' }console.log(s.name);    // 小王

在静态方法中,能够通过 super 调用父类上定义的静态方法:

class Person {    constructor(name) {        this.name = name;    }    static of(name) {        return new Person(name);    }}class Student extends Person{    constructor(name) {        super(name);        console.log(this);    }    static of(name) {        return  super.of(name);    }}let s = Student.of("小军");console.log(s);    // Person { name: '小军' }console.log(s instanceof Student);    // falseconsole.log(s instanceof Person);    // true

ECMAScript 能够通过 new.target 实现一个供其余类继承,但自身不会被实例化的形象基类。new.target 保留通过 new 关键字调用的类或函数。通过在实例化时检测new.target 是不是形象基类,能够阻止对形象基类的实例化:

class Person {    constructor(name) {        if (new.target === Person) {            throw new Error("Person不能间接被实例化");        }        this.name = name;    }}class Student extends Person {    constructor(name) {        super(name);    }}let s = new Student("小萍");let p = new Person("小玲");    // Error: Person不能间接被实例化

也能够在形象基类的构造方法中查看子类是否定义了某个办法。因为原型办法在调用类构造方法之前就曾经存在了,所以能够通过 this 来查看相应的办法:

class Person {    constructor(name) {        if (new.target === Person) {            throw new Error("Person不能间接被实例化");        }        if (!this.action) {            throw Error("继承的类必须定义action()办法");        }        this.name = name;    }}class Student extends Person {    constructor(name) {        super(name);    }    action() {        console.log("学生行为");    }}class Employee extends Person {    constructor(name) {        super(name);    }}let s = new Student("小萍");let e = new Employee("小洪");    // Error: 继承的类必须定义action()办法

ES6 新增的 classextends 能够很顺畅的为内置援用类型扩大性能。

class MoreArray extends Array {    first() {        return this[0];    }    last() {        return this[this.length - 1];    }}let arr = new MoreArray(20, 92, 15, 40);console.log(arr.first());    // 20console.log(arr.last());    // 40

类混入

ECMAScript 6 反对单继承,然而能够通过现有个性模仿多重继承。

extends 关键字前面能够跟一个 JavaScript 表达式。任何能够解析为一个类或一个构造函数的表达式都是无效的。

class Person {}function getPerson() {    console.log("对象操作");    return Person;}class Student extends getPerson() {}    // 对象操作

混入模式能够通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个能够被继承的类。如果 Person 类须要组合A、B、C,则须要某种机制实现 B 继承 A,C 继承 B,而 Person 再继承 C,从而把 A、B、C 组合到 Person 中。实现这种模式有不同的策略。

一个策略是定义一组 “可嵌套” 的函数,每个函数别离接管一个父类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数能够连缀调用,最终组合成超类表达式:

class Person {}let Action = (Superclass) => class extends Superclass {    action() {        console.log('动作');    }};let Face = (Superclass) => class extends Superclass {    face() {        console.log('表情');    }};let Sex = (Superclass) => class extends Superclass {    sex() {        console.log('性别');    }};class Student extends  Sex(Face(Action(Person))) {}let s = new Student();s.action();    // 动作s.face();    // 表情s.sex();    // 性别

也能够通过写一个辅助函数,能够把嵌套调用开展:

通过写一个辅助函数,能够把嵌套调用开展:

class Person {}let Action = (Superclass) => class extends Superclass {    action() {        console.log('动作');    }};let Face = (Superclass) => class extends Superclass {    face() {        console.log('表情');    }};let Sex = (Superclass) => class extends Superclass {    sex() {        console.log('性别');    }};function mix(BaseClass, ...Mixins) {    return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);}class Student extends mix(Person, Action, Face, Sex) {}let s = new Student();s.action();    // 动作s.face();    // 表情s.sex();    // 性别

留神:很多JavaScript框架曾经摈弃混入模式,转向组合模式。这反映了 “组合胜过继承” 的软件设计准则,且提供了很大的灵活性。

总结

ECMAScript 6 新增的类语法很大水平上是基于语言既有的原型机制来实现的语法糖。但这种语法能够优雅地定义向后兼容的类,既能够继承内置类型,也能够继承自定义类型。类无效地逾越了对象实例、原型和类之间的鸿沟。

更多内容请关注公众号「海人为记