1. 创建 class
    1. class declarations
    2. class expression
  2. constructor
  3. 公有属性/方法
  4. 静态属性/方法
  5. 私有属性/方法
  6. super 关键字
  7. class 的本质
    1. 子类继承
  8. 一些坑
  9. 参考

class

Es6 开始引入了 class 关键字,其本质是基于构造函数和原型链继承的语法糖.

创建 class

有两种方法创建 class, class declartions(类声明) 和 class expression(类表达式).

class declarations

1
class Foo {}

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 的 prototype 属性上, 不能等价于如下实现:

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()); // 1

私有属性/方法只能通过 .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()); // undefined

私有属性/方法

当前私有属性/方法位于 Stage-3 阶段,需 配合 Babel 才能在浏览器环境中使用.
也可以在 node.js(>13.2.0) 中用 harmony 参数开启私有方法的支持.

1
node --harmony index.js

私有属性/方法只能在 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()); // 1
// 在 class 外调用私有属性会报错
console.log(t.#privateProperty) // SyntaxError: Private field '#privateProperty' must be declared in an enclosing class

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(); // Child {bar: ƒ}

如果子类中有 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(); // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

错误的例子, 没有调用 super

1
2
3
4
5
6
7
class Parent {}

class Child extends Parent {
constructor() {}
}

new Child(); // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

没有 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) // SyntaxError: 'super' keyword unexpected here
}
}

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");
// 但是这个 this 会指向子类实例
console.log(this.name); // c
}
}

class Child extends Parent {
constructor() {
super();
this.name = "c";
super.printName(); // parent function // c
}
// 这个不会被调用
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");
// 但是这个 this 会指向子类实例
console.log(this.name); // c
}
}

class Child extends Parent {
constructor() {
super();
this.name = "c";
}
// 这个不会被调用
printName() {
console.log("child function");
console.log(this.name);
}
s() {
super.printName(); // parent function // c
}
}

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"); // parent function
console.log(this.name); // Child
}
}

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); // function

和构造函数一样,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) // 2
t.getFoo // TypeError: t.getFoo is not a function

class 内的静态属性/方法(static 开头)绑在 class 对象上, 而普通方法绑在 class 对象的 prototype 属性上.

1
2
3
4
5
class T {
static foo() {}
bar() {}
}
console.dir(T);

158t785s45.PNG

相当于:

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 {foo: 1, #bar: 2}
t.__proto__; // {constructor: ƒ, getFoo: ƒ}

32b22e04.png

子类继承

通过子类生成实例时,父类和子类上的对象都会绑定在新生成实例对象上.

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); // {name: "c", foo: "bar"}

相当于:

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); // {name: "c", foo: "bar"}

子类实例 的方法绑定在 __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(); // c
t.__proto__.__proto__.name(); // p

t.__proto__ === Child.prototype; // true
Child.prototype.__proto__ === Parent.prototype; // true
Child.__proto__ === Parent; // true

164d3695.png

相当于

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(); // c
t.__proto__.__proto__.name(); // p

t.__proto__ === Child.prototype; // true
Child.prototype.__proto__ === Parent.prototype; // true
Child.__proto__ === Parent; // true

一些坑

1 foo = 1 相当于 this.foo = 1.直接对 foo 取值会报错,不要把 class 的大括号内部当成函数的作用域.

1
2
3
4
5
class T {
foo = 1;
bar = `${foo}`;
}
t = new T(); // ReferenceError: foo is not defined

解决办法:

1
2
3
4
5
class T {
foo = 1;
bar = `${this.foo}`;
}
t = new T(); // {foo: 1, bar: "1"}

这就产生了一些歧义, 当 this 在等式左边的时候相当于 this.this .而在右边的时候还是 this 本身.

1
2
3
4
5
6
class T {
a = 0;
this = 1;
b = this.a;
}
t = new T(); // {a: 0, this: 1, b: 0};

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); // child
c.printName(); // child

但是当 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); // child
console.log(this.#name); // parent
}
}
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"; // 这个 name 在实例对象上
}
class Child extends Parent {
#name = "child";
get name() { // 这个 name 在 prototype 上
return this.#name;
}
}
c = new Child();
console.log(c.name); // parent

158d885.PNG

同理, 父类上的方法也可能被子类的属性覆盖.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Parent {
#num = 0;
get num() { // 这个 name 在 prototype 上, 被子类的 num 覆盖了
return this.#num;
}
add(x) {
this.#num += x;
}
}
class Child extends Parent {
num = 5;
}
c = new Child();
console.log(c.num); // 5
c.add(1);
console.log(c.num); // 5
console.log(c) // Child {num: 5, #num: 1}

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';
// private bar = 'child bar'; // error: Class 'Child' incorrectly extends base class 'Parent'.
}

let p = new Parent();
console.log(p.getFoo()); // parent foo
console.log(p.getBar()); // parent bar
let c = new Child();
console.log(c.getFoo()); // child parent
console.log(c.getBar()); // parent bar

参考

Public and private class fields · V8
ES6 入门教程
Classes - JavaScript | MDN
ES6的子类有没有自己的this? - 知乎