Es6 开始引入了 class 关键字,其本质是基于构造函数和原型链继承的语法糖.
创建 class
有两种方法创建 class, class declartions(类声明) 和 class expression(类表达式).
class declarations
class declarations 必须有类名,否则会报错 SyntaxError.
1 class {} // SyntaxError: Unexpected token {
和 let const 一样, class 不会被提升到作用域顶部, 也会形成 temporal dead zone(暂时性死区, 就是没创建前不能用).
1 2 console.log(Foo); // ReferenceError: Foo is not defined class Foo {}
因为 temporal dead zone 的影响, 未声明 class 前就用 typeof 的话会报错.
1 typeof Foo; // "undefined"
1 2 typeof Foo; // ReferenceError: Foo is not defined class Foo {}
class 内默认为 strict mode (严格模式)
1 2 3 4 5 6 class Foo { constructor() { i = 1; } } new Foo(); // ReferenceError: i is not defined
class expression
1 2 3 let Foo = class {}; // or let Bar = class Foo {};
class expression 可以没有类名. 如果有类名, 则这个类名只能在类中使用,不能在类外面使用.
1 2 3 4 5 6 7 8 let Foo = class Bar { static printBar() { console.log(Bar); } }; Foo.printBar(); // class Bar { ... // 访问不到 console.log(Bar); // ReferenceError: Bar is not defined
const 和 let 声明的 class 不会被提升到作用域顶部.
1 2 console.log(Foo); // ReferenceError: Foo is not defined let Foo = class {};
但如果用了 var, class expression 会被提升.
1 2 console.log(Foo); // undefined var Foo = class {};
constructor
constrcutor 方法是 class 内的特有方法, 用于初始化对象.
1 2 3 4 5 6 class Person { constructor(name, age) { this.name = name; this.age = age; } }
等价于
1 2 3 4 5 6 7 8 9 function Person(name, age) { if (!(this instanceof Person)) { throw TypeError( "TypeError: Class constructor Foo cannot be invoked without 'new'" ); } this.name = name; this.age = age; }
当用 new 关键字调用 Person 时,会自动执行内部的 constructor 函数,然后返回一个对象.
1 2 let anna = new Person("anna", 18); // Person {name: "anna", age: 18} console.log(anna.name);
公有属性/方法
属性:
1 2 3 4 5 6 7 8 9 10 11 class T { foo = 1 } class T { constructor ( ) { this .foo = 1 } }
方法
1 2 3 4 5 6 7 8 9 10 11 class T { bar ( ) {} } class T { constructor ( ) { this .bar = function ( ) {} } }
静态属性/方法
静态属性/方法可以直接在 class 上调用,但不能在 class 的实例上调用.
1 2 3 4 5 6 7 8 class T { static staticProperty = 1 ; static staticMethod ( ) { return this .staticProperty ; } } console .log (T.staticMethod ());
私有属性/方法只能通过 .propName 的形式访问, 不能通过 [propName] 的形式访问.
1 2 3 4 5 6 7 8 9 10 class T { #privateProperty = 1 ; get ( ) { return this ['#privateProperty' ]; } } t = new T (); console .log (t.get ());
私有属性/方法
当前私有属性/方法位于 Stage-3 阶段,需 配合 Babel 才能在浏览器环境中使用.
也可以在 node.js(>13.2.0) 中用 harmony 参数开启私有方法的支持.
私有属性/方法只能在 class 的内部调用,在外面调用会报错.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class T { #privateProperty = 1 ; #privateMethod ( ) { return this .#privateProperty; } getPrivatePorperty ( ) { return this .#privateMethod (); } } t = new T (); console .log (t.getPrivatePorperty ()); console .log (t.#privateProperty)
super 关键字
super 关键字有两种调用方式:
1 super() 相当于调用父类的 constructor, 只能在子类的 constructor 中使用.在其他方法中这样用时会报错.
2 super.propName or super["propName"] 访问父类 prototype 属性上的方法(例外: 在静态方法中调用时指向父类自身)
例1:
在子类的 constructor 中调用 super
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Parent { constructor (foo, bar ) { this .foo = foo; this .bar = bar; } } class Child extends Parent { constructor (foo, bar ) { super (foo, bar); } } new Child (1 , 2 );
例2:
用 super 把父类的方法绑定到子类实例上.
1 2 3 4 5 6 7 8 9 class Parent { foo ( ) {} } class Child extends Parent { bar = super .foo ; } new Child ();
如果子类中有 constructor 方法,则需要在 constructor 中手动调用 super.
在子类的 constructor 调用 super 之前,不可以访问 this.
正确的例子:
1 2 3 4 5 6 7 8 9 class Parent {}class Child extends Parent { constructor ( ) { super (); } } new Child ();
错误的例子, 在调用 super 前访问了 this.
1 2 3 4 5 6 7 8 9 10 class Parent {}class Child extends Parent { constructor ( ) { console .log (this ); super (); } } new Child ();
错误的例子, 没有调用 super
1 2 3 4 5 6 7 class Parent {}class Child extends Parent { constructor ( ) {} } new Child ();
没有 constructor 方法的话, 相当于默认自动在 constructor 调用了 super.
1 2 3 class Parent {}class Child extends Parent {}
注意: super 是个关键字, 而非函数内的属性.不可以用如下方式调用 super
1 2 3 4 5 6 7 8 9 10 class Parent { }class Child extends Parent { constructor ( ) { super () console .log (super ) } } let ci = new Child ();
在子类的方法内调用 super 上的方法时, 调用的是父类对象的 prototype 属性上的方法(以下称: 方法 a).但此时方法 a 内的 this 指向子类实例.
constructor 内调用 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 class Parent { constractor ( ) { this .name = "p" ; } printName ( ) { console .log ("parent function" ); console .log (this .name ); } } class Child extends Parent { constructor ( ) { super (); this .name = "c" ; super .printName (); } printName ( ) { console .log ("child function" ); console .log (this .name ); } } let ci = new Child ();
普通方法内调用 super 方法, 和 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 28 29 class Parent { constractor ( ) { this .name = "p" ; } printName ( ) { console .log ("parent function" ); console .log (this .name ); } } class Child extends Parent { constructor ( ) { super (); this .name = "c" ; } printName ( ) { console .log ("child function" ); console .log (this .name ); } s ( ) { super .printName (); } } let ci = new Child ();ci.s ();
静态方法内的 super 指向父类对象, 所以没法访问到 prototype 上的方法(没找到时会去 __proto__ 上接着找).
但此时方法内的 this 指向子类对象.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Parent { name = "p" ; static printName ( ) { console .log ("parent function" ); console .log (this .name ); } } class Child extends Parent { name = "c" ; static printName ( ) { console .log ("child function" ); console .log (this .name ); } static s ( ) { super .printName (); } } Child .s ();
class 的本质
class 是构造函数的语法糖,本质上也是个对象.
1 2 class T {}console .log (typeof T);
和构造函数一样,class 也可以通过在 contructor 中返回一个对象,取代原本的 this.
1 2 3 4 5 6 7 8 9 10 11 12 class T { constructor ( ){ this .foo = 1 ; return {foo : 2 }; } getFoo ( ) { return this .foo }; } t = new T (); console .log (t.foo ) t.getFoo
class 内的静态属性/方法(static 开头)绑在 class 对象上, 而普通方法绑在 class 对象的 prototype 属性上.
1 2 3 4 5 class T { static foo ( ) {} bar ( ) {} } console .dir (T);
相当于:
1 2 3 function T ( ) {}T.foo = function ( ) {}; T.prototype .bar = function ( ) {};
class 中的普通属性和私有属性绑定在 class 生成的实例对象(以下称为: 对象 a)上, 对象 a 可以通过 __proto__ 访问 class 的 prototype 属性,因此可以通过 对象 a 调用 class 的普通方法,但不能通过 对象 a 调用 class 的静态方法.
1 2 3 4 5 6 7 8 9 10 class T { foo = 1 ; #bar = 2 ; getFoo ( ) { return this .foo ; } } const t = new T ();console .log (t); t.__proto__ ;
子类继承
通过子类生成实例时,父类和子类上的对象都会绑定在新生成实例对象上.
1 2 3 4 5 6 7 8 9 10 class Parent { name = "p" ; foo = "bar" ; } class Child extends Parent { name = "c" ; } t = new Child (); console .log (t);
相当于:
1 2 3 4 5 6 7 8 9 10 11 12 function Parent ( ) { this .name = "p" ; this .foo = "bar" ; } function Child ( ) { Parent .call (this ); this .name = "c" ; } Object .setPrototypeOf (Child , Parent );Object .setPrototypeOf (Child .prototype , Parent .prototype );t = new Child (); console .log (t);
子类实例 的方法绑定在 __proto__ 上, 也就是子类的 prototype 属性上.
子类的 prototype.__proto__ 属性指向父类的 prototype, 从而实现了方法的继承.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Parent { name ( ) { console .log ("p" ); } foo ( ) {} } class Child extends Parent { name ( ) { console .log ("c" ); } } t = new Child (); t.__proto__ .name (); t.__proto__ .__proto__ .name (); t.__proto__ === Child .prototype ; Child .prototype .__proto__ === Parent .prototype ; Child .__proto__ === Parent ;
相当于
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function Parent ( ) {}Parent .prototype .name = function ( ) { console .log ("p" ); }; Parent .prototype .foo = function ( ) {};function Child ( ) {}Child .prototype .name = function ( ) { console .log ("c" ); }; Object .setPrototypeOf (Child , Parent );Object .setPrototypeOf (Child .prototype , Parent .prototype );t = new Child (); t.__proto__ .name (); t.__proto__ .__proto__ .name (); t.__proto__ === Child .prototype ; Child .prototype .__proto__ === Parent .prototype ; Child .__proto__ === Parent ;
一些坑
1 foo = 1 相当于 this.foo = 1.直接对 foo 取值会报错,不要把 class 的大括号内部当成函数的作用域.
1 2 3 4 5 class T { foo = 1 ; bar = `${foo} ` ; } t = new T ();
解决办法:
1 2 3 4 5 class T { foo = 1 ; bar = `${this .foo} ` ; } t = new T ();
这就产生了一些歧义, 当 this 在等式左边的时候相当于 this.this .而在右边的时候还是 this 本身.
1 2 3 4 5 6 class T { a = 0 ; this = 1 ; b = this .a ; } t = new T ();
2 当 name 是普通属性时, printName 中的 this 指向子类实例, this.name 值为 "child".
1 2 3 4 5 6 7 8 9 10 11 12 class Parent { name = "parent" ; printName ( ) { console .log (this .name ); } } class Child extends Parent { name = "child" ; } c = new Child () console .log (c.name ); c.printName ();
但是当 name 是私有属性时, printName 中的 this 还是指向子类实例, 但是获取到的 this.#name 却是父类上的.
1 2 3 4 5 6 7 8 9 10 11 12 class Parent { #name = "parent" ; printName ( ) { console .log (this ); console .log (this .#name); } } class Child extends Parent { #name = "child" ; } c = new Child (); c.printName ();
3 父类/子类的属性都绑定在实例对象上,方法绑定在类的 prototype 上.因此子类的方法可能会被父类的同名属性覆盖掉.
给子类加方法的时候一定要确保父类里没有同名的属性.
1 2 3 4 5 6 7 8 9 10 11 class Parent { name = "parent" ; } class Child extends Parent { #name = "child" ; get name () { return this .#name; } } c = new Child (); console .log (c.name );
同理, 父类上的方法也可能被子类的属性覆盖.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Parent { #num = 0 ; get num () { return this .#num; } add (x ) { this .#num += x; } } class Child extends Parent { num = 5 ; } c = new Child (); console .log (c.num ); c.add (1 ); console .log (c.num ); console .log (c)
4 TypeScript 也有两个类似私有领域(#)的关键字: private protected, 和 # 的区别如下:
1 protected: 如果子类上有就用子类上的,否则用父类上的.
2 private: ts 不允许在子类上创建同名的 private,否则会报错.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Parent { protected foo = 'parent foo' ; private bar = 'parent bar' ; getFoo ( ) { return this .foo ; } getBar ( ) { return this .bar ; } } class Child extends Parent { protected foo = 'child parent' ; } let p = new Parent ();console .log (p.getFoo ()); console .log (p.getBar ()); let c = new Child ();console .log (c.getFoo ()); console .log (c.getBar ());
参考
Public and private class fields · V8
ES6 入门教程
Classes - JavaScript | MDN
ES6的子类有没有自己的this? - 知乎