class

class是一个语法糖,其底层还是通过 构造函数 去创建的。新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

es5实现

1
2
3
4
5
6
7
8
9
function Point(x, y) {
this.x = x;
this.y = y;
}

Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);

es6实现

1
2
3
4
5
6
7
8
9
10
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString () {
return '(' + this.x + ', ' + this.y + ')';
}
}
var p = new Point(1, 2);

上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5 的构造函数Point,对应 ES6 的Point类的构造方法。

constructor 方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

1
2
3
4
5
6
7
class A {
}

// 等同于
class A {
constructor() {}
}

constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。

1
2
3
4
5
6
7
8
class A {
constructor() {
return Object.create(null);
}
}

console.log((new A()) instanceof A);
// false

类的实例

  • 生成类的实例的写法,与 ES5 完全一样,也是使用 new 命令。前面说过,如果忘记加上new,像函数那样调用Class,将会报错。
  • 与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class类上)
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 CppPerson {
constructor({name, age, fn = () => {}}) {
this.name = name
this.age = age
this.showFn = fn
}
getName() {
const name = this.name
return name.charAt(0).toUpperCase() + name.slice(1)
}
}
const person = new CppPerson({
name: 'cpp',
age: '30',
fn: () => {
console.log('this is fn');
}
})
person.getName()
person.showFn()
console.log('person', person);
person.hasOwnProperty('name') // true
person.hasOwnProperty('age') // true
person.hasOwnProperty('getName') // false
person.__proto__.hasOwnProperty('getName') // true

与 ES5 一样,类的所有实例共享一个原型对象。

静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

1
2
3
4
5
6
7
8
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()// TypeError: foo.classMethod is not a function

实例属性的新写法

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。

1
2
3
4
5
6
7
8
9
10
class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}

突然联想到目前的业务项目,做了2年才发现也是这样的写法,

1
2
private item: number = 0 // 快捷导航选中index
private url: any = {}; // 默认url赋值

上面代码中,实例属性item与原型上的方法,处于同一个层级。这时,不需要在实例属性前面加上this。
这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。
原本应该是这样写的

1
2
3
4
5
6
default class VueGrid extends Vue {
constructor() {
this.item = 0;
this.url = {}
}
}

静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

1
2
3
4
5
class Foo {
}

Foo.prop = 1;
Foo.prop // 1

上面的写法为Foo类定义了一个静态属性prop。

目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上static关键字。

1
2
3
4
5
6
7
class MyClass {
static myStaticProp = 42;

constructor() {
console.log(MyClass.myStaticProp); // 42
}
}

这个新写法大大方便了静态属性的表达。

1
2
3
4
5
6
7
8
9
10
// 老写法
class Foo {
// ...
}
Foo.prop = 1;

// 新写法
class Foo {
static prop = 1; // 新写法是显式声明(declarative),而不是赋值处理,语义更好。
}

extends继承

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

1
2
class Point {}
class ColorPoint extends Point {}

子类必须在constructor方法中调用super方法,否则新建实例时会报错。
这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

super

在 super() 执行时,它指向的是 子类 B 的构造函数,而不是父类 A 的构造函数。也就是说,super() 内部的 this 指向的是 B
super()相当于Parent.prototype.constructor.call(this. props)
super本身是指向父类的构造函数但做函数调用后返回的是子类的实例,实际上做了parent.prototype.constructor.call(this),做对象调用时指向父类 parent.prototype,从而实现继承

super这个关键字,既可以当作函数 Function 使用,也可以当作对象 Object 使用。在这两种情况下,它的用法完全不同。

作函数 Function 使用

代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。

1
2
3
4
5
6
7
8
9
10
11
12
13

class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B

虽然代表父类的构造函数,但是返回的是子类的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。

有点绕!!!

当作对象 Object 使用

super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
p() {
return 2;
}
}

class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();

子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。注意是A类原型对象上的方法。
这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test {
name() {
return 'cpp'
}
}
// 如果属性定义在父类的原型对象上,super就可以取到。
Test.prototype.age = 29
class Test1 extends Test {
constructor() {
super()
console.log(super.name())
console.log(super.age)
}
}
console.log(new Test1()) // 'cpp' 29

实例属性和方法: constructor里的,就是绑定的this,比如this.name
原型属性和方法: class里除了构造函数以内,其他的方法就是原型方法,原型属性需要用到Test.prototype来定义
静态属性和方法: static关键字标识,只能类本身调用,类的实例不能调用
私有属性和方法: ts里的private关键字

ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let b = new B();
b.m() // 2

如果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
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}

class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}

myMethod(msg) {
super.myMethod(msg);
}
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

上面代码中,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

ES5 和 ES6继承区别

ES5 的继承,通过prototype或构造函数机制来实现, 实质是先创造子类的实例对象this,再将父类的方法添加到this上面(Parent.apply(this))。
ES6 的继承机制完全不同,实质是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。

另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}

class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}

参考链接