ECMAScript 6 引入的 class 关键字具有正式定义类的能力,但实际上使用的仍然是原型和构造函数的概念
类定义 有两种主要方式定义类:类声明和类表达式
1 2 3 4 5 class Person {}const Animal = class {};
函数表达式和类表达式在求值前都不能引用
类定义不能提升,但函数定义可以
函数受函数作用域 限制,类受块作用域 限制
1 2 3 4 5 6 7 8 9 { function FunctionDeclaration ( ) {} class ClassDeclaration {} }console .log (FunctionDeclaration );console .log (ClassDeclaration );
1 2 3 4 5 6 7 8 9 10 11 12 let Person = class PersonName { identify ( ) { console .log (Person .name , PersonName .name ); } }let p = new Person (); p.identify (); console .log (Person .name ); console .log (PersonName );
类构造函数
实例化 使用 new 调用类的构造函数会执行创建对象 的过程
类实例化时传入的参数会用作构造函数的参数
默认情况下,类构造函数会在执行后返回 this 对象,被用作实例化的对象
如果返回的不是 this 对象,而是其他对象,对这个对象使用 instanceof 操作符返回结果为 false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Person { constructor (override ) { this .foo = 'foo' ; if (override) { return { bar : 'bar' }; } } }let p1 = new Person (), p2 = new Person (true );console .log (p1); console .log (p1 instanceof Person ); console .log (typeof p1); console .log (p2); console .log (p2 instanceof Person ); console .log (typeof p2);
调用类构造函数必须使用 new 操作符,不使用 new 会抛出错误
普通构造函数如果不使用 new 调用,就会以全局的 this(通常是 window)作为内部对象
实例化后,类构造函数会成为普通的实例方法(仍需使用 new 调用),可以在实例上引用它
1 2 3 4 5 6 7 8 9 class Person {}let p1 = new Person ();let p2 = new p1.constructor ( );
把类当成特殊函数 ECMAScript 类是一种特殊的函数
1 2 3 4 5 6 7 8 class Person {}console .log (Person ); console .log (typeof Person ); console .log (Person .prototype ); console .log (Person === Person .prototype .constructor );
类中定义的 constructor 方法不会 被当成构造函数,对它使用 instanceof 操作符返回 false。如果在创建实例时直接将类构造函数当成普通构造函数 ,instanceof 操作符的返回值会反转
1 2 3 4 5 6 7 8 9 10 11 12 let p1 = new Person ();console .log (p1.constructor === Person ); console .log (p1 instanceof Person ); console .log (p1 instanceof Person .constructor ); let p2 = new Person .constructor ( );console .log (p2.constructor === Person ); console .log (p2 instanceof Person ); console .log (p2 instanceof Person .constructor );
类可以像其他对象或函数引用一样作为参数传递
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let classList = [ class { constructor (id ) { this .id_ = id; console .log (`instance ${this .id_} ` ); } } ];function createInstance (classDefinition, id ) { return new classDefinition (id); }let foo = createInstance (classList[0 ], 3141 );
类可以像立即调用函数表达式一样立即实例化
1 2 3 4 5 6 7 8 9 let p = new class { constructor (x ) { console .log (x); } }('bar' ); console .log (p);
实例、原型和类成员 实例 在构造函数内部,可以为新创建的实例(this)添加“自有”属性。构造函数执行完也可以给实例继续添加新成员
每个实例都对应一个唯一的成员对象,所有成员都不会在原型上共享
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Person { constructor ( ) { this .name = new String ('Jack' ); this .sayName = () => console .log (this .name ); this .nicknames = ['Jake' , 'J-Dog' ] } }let p1 = new Person (), p2 = new Person (); p1.sayName (); p2.sayName (); console .log (p1.name === p2.name ); console .log (p1.sayName === p2.sayName ); console .log (p1.nicknames === p2.nicknames ); p1.name = p1.nicknames [0 ]; p2.name = p2.nicknames [1 ]; p1.sayName (); p2.sayName ();
原型方法与访问器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 const symbolKey = Symbol ('symbolKey' );class Person { constructor ( ) { this .locate = () => console .log ('instance' ); } locate ( ) { console .log ('prototype' ); } stringKey ( ) { console .log ('invoked stringKey' ); } [symbolKey]() { console .log ('invoked symbolKey' ); } ['computed' + 'Key' ]() { console .log ('invoked computedKey' ); } set name (newName ) { this .name_ = newName; } get name () { return this .name_ ; } }let p = new Person (); p.locate (); Person .prototype .locate (); p.stringKey (); p[symbolKey](); p.computedKey (); p.name = 'Jake' ;console .log (p.name );
静态类方法 静态方法通常用于执行不特定于实例的操作,也不要求存在类的实例,每个类只能有一个静态成员 (类级别的属性)
静态类成员在类定义中使用 static 关键字作为前缀,使用 this 引用类自身
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Person { constructor ( ) { this .locate = () => console .log ('instance' , this ); } locate ( ) { console .log ('prototype' , this ); } static locate ( ) { console .log ('class' , this ); } }let p = new Person (); p.locate (); Person .prototype .locate (); Person .locate ();
静态类方法适合作为实例工厂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Person { constructor (age ) { this .age_ = age; } sayAge ( ) { console .log (this .age_ ); } static create ( ) { return new Person (Math .floor (Math .random ()*100 )); } }console .log (Person .create ());let p1 = new Person (29 );console .log (p1.sayAge ());
非函数原型和类成员 ECMAScript 2022 允许直接在类体中定义数据成员,无需在 constructor 中定义
在类定义外部,可以在原型或类上手动添加成员数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Person { age = 0 ; constructor (name ) { this .name = name; } sayJob ( ) { console .log (`${Person.greeting} ${this .job} ` ); } }const p = new Person ("Alice" );console .log (p.name ); console .log (p.age ); Person .greeting = 'My job is' ;Person .prototype .job = 'Software Engineer' ;let p1 = new Person ('Jake' ); p1.sayJob ();
迭代器与生成器方法 类定义语法支持在原型和类本身上定义生成器方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 class Person { constructor ( ) { this .nicknames = ['cJack' , 'cJake' , 'cJ-Dog' ]; } [Symbol .iterator ]() { return this .nicknames .entries (); } *creatNicknameIterator ( ) { yield 'Jack' ; yield 'Jake' ; yield 'J-Dog' ; } static *createJobIterator ( ) { yield 'Butcher' ; yield 'Baker' ; yield 'Candlestick maker' ; } }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.creatNicknameIterator ();console .log (nicknameIter.next ().value ); console .log (nicknameIter.next ().value ); console .log (nicknameIter.next ().value ); for (let [idx, nickname] of p) { console .log (nickname); }
继承 ECMAScript 6 原生支持类继承机制,但背后依旧使用原型链
继承基础 ES6 支持单继承 。使用 extends 关键字,可以继承任何拥有 [[Construct]] 和原型 的对象。不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Vehicle { identifyPrototype (id ) { console .log (id, this ); } static identifyClass (id ) { console .log (id, this ); } }class Bus extends Vehicle {}let b = new Bus ();console .log (b instanceof Bus ); console .log (b instanceof Vehicle ); let v = new Vehicle (); b.identifyPrototype ('bus' ); v.identifyPrototype ('vehicle' ); Bus .identifyClass ('bus' ); Vehicle .identifyClass ('vehicle' ); function Person ( ) {}class Engineer extends Person {}let e = new Engineer ();console .log (e instanceof Engineer ); console .log (e instanceof Person ); let Bar = class extends Foo {}
构造函数、HomeObject 和 super() 可以使用 super 关键字引用派生类方法的原型
super 关键字只能在派生类中使用
仅限类构造函数、实例方法和静态方法内部使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Vehicle { constructor ( ) { this .hasEngine = true ; } static identify ( ) { console .log ('vehicle' ); } }class Bus extends Vehicle { constructor ( ) { super (); console .log (this instanceof Vehicle ); console .log (this ); } static identify ( ) { super .identify (); } }new Bus ();Bus .identify ();
使用 super 注意事项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class Vehicle { constructor (licensePlate ) { this .licensePlate = licensePlate; } }class Bus extends Vehicle { constructor (licensePlate ) { super (licensePlate); } }class Bus1 extends Vehicle { constructor ( ) { super (); console .log (this instanceof Vehicle ); } }class Van extends Vehicle { constructor ( ) { return {}; } }class Bus2 extends Vehicle {}new Bus1 (); console .log (new Bus ('1337H4X' )); console .log (new Bus2 ('1337H4X' )); console .log (new Van ());
抽象基类 通过在实例化时检测 new.target(new.target 保存通过 new 关键字调用的类或函数) 是不是抽象基类,可以阻止对抽象基类的实例化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Vehicle { constructor ( ) { console .log (new .target ); if (new .target === Vehicle ) { throw new Error ('Vehicle cannot be directly instantiated' ); } if (!this .foo ) { throw new Error ('Inheriting class must define foo()' ); } console .log ('success!' ); } }class Bus extends Vehicle { foo ( ) {} }class Van extends Vehicle {}new Bus ();new Vehicle ();
继承内置类型 可以扩展内置类型,覆盖 Symbol.species 访问器(决定在创建返回实例时使用的类)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class SuperArray extends Array { shuffle ( ) { for (let i = this .length - 1 ; i > 0 ; i--) { const j = Math .floor (Math .random () * (i + 1 )); [this [i], this [j]] = [this [j], this [i]]; } } static get [Symbol .species ]() { return Array ; } }let a = new SuperArray (1 , 2 , 3 , 4 , 5 );console .log (a instanceof Array ); console .log (a instanceof SuperArray ); console .log (a); a.shuffle ();console .log (a); let a1 = new SuperArray (1 , 2 , 3 , 4 , 5 );let a2 = a1.filter (x => !!(x%2 ))console .log (a1); console .log (a2); console .log (a1 instanceof SuperArray ); console .log (a2 instanceof SuperArray );
多类继承 ES6 没有显式支持多类继承,但通过现有特性可模拟多类继承
定义一组“可嵌套”的函数,每个函数分别接收一个超类(被其他类继承的 “父类”)作为参数,将混入类 定义为该参数的子类,并返回这个类。可以连续调用组合函数,组成超类表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class Vehicle {}let FooMixin = (Superclass ) => class extends Superclass { foo ( ) { console .log ('foo' ); } };let BarMixin = (Superclass ) => class extends Superclass { bar ( ) { console .log ('bar' ); } };let BazMinxin = (Superclass ) => class extends Superclass { baz ( ) { console .log ('baz' ); } };function mix (BaseClass, ...Mixins ) { return Mixins .reduce ((accumulator, current ) => current (accumulator), BaseClass ); }class Bus extends FooMixin (BarMixin (BazMinxin (Vehicle ))) {}class Bus1 extends mix (Vehicle , FooMixin , BarMixin , BazMinxin ) {}let b = new Bus (); b.foo (); b.bar (); b.baz ();
很多 JavaScript 框架(React)已抛弃混入模式,转向组合模式(把方法提取到独立的类和辅助对象,再把它们组合起来,但不使用继承)