0%

ES6 的 class 学习

ES6 class

在ES6之前,JavaScript 是没有类的概念。 只能通过构造函数模拟类来生成实例。也就是直接使用原型对象模仿面向对象中的类和类继承。

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

Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};

let p = new Point(1, 2);

但ES6起,JavaScript正式引入了class关键字,自此我们也可以通过class来定义类了。

但需要清楚的是ES6中class只是构造函数的一种语法糖,并非新鲜玩意,class能实现的,我们通过ES5构造函数同样可以实现。所以ES6 的类,完全可以看作构造函数的另一种写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Point{
constructor(x, y) {
this.x = x;
this.y = y;
};
toString() {
return '(' + this.x + ', ' + this.y + ')';
};
}
let p = new Point(1, 2);

typeof Point // "function" 实际还是通过构造函数模拟类来生成实例,只是写法更加方便
Point === Point.prototype.constructor // true

console.log(Point.prototype);
/*
{constructor: ƒ, toString: ƒ}
constructor: class Point
toString: ƒ toString()
__proto__: Object
*/

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

ES5中原型上的方法toString,在ES6 class写法中直接写在了内部,同时省略了function关键字。另外,方法之间不需要逗号分隔,加了会报错。

ES6 类内部写的方法依旧是写在 protoype 上。

类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。

1
2
3
4
5
6
7
8
function Point(x, y) {
}
Point(); //undefiend

class Foo {
}

Foo() //Uncaught TypeError: Class constructor Foo cannot be invoked without 'new'

类不存在提升,只是声明后在使用。类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式

类的表现形式

类的表现形式:声明式和表达式。其实和函数的表现形式一摸一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//声明式
class A{
constructor(){}
}

// 匿名表达式
const A = class {
constructor(){}
}

//命名表达式
const B = class B1 {
constructor(){}
}

constructor 方法

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

1
2
3
4
5
6
7
class Point {
}

// 等同于
class Point {
constructor() {} // 自动调用,并且constructor方法默认返回实例对象(即this),
}

既然 constructor函数默认返回实例对象,也可以指定返回一个全新的对象,结果导致实例对象不是Point类的实例。

1
2
3
4
5
6
7
class Point {
constructor() {
this.x='y';
return Object.create(null);
}
}
console.log(new Point() instanceof Point) // false

class的prototype

使用 function 构造函数可以使用 prtotype 属性一次批量在原型上添加方法,但是,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
26
class PointOne {
constructor() {}
toString () {return 'string-1'}
toValue() {return 'value-1'}
};
PointOne.prototype.name = 'PointOne';
PointOne.prototype.toName = function(){ return this.name};
let po = new PointOne();

po.name; // PointOne
po.toString(); // string-1
po.toValue(); // value-1
po.toName(); // PointOne

class PointTwo {};
PointTwo.prototype.name = 'PointTwo';
PointOne.prototype.toName = function(){ return this.name};
PointTwo.prototype = {
toStr: function () { return 'string-2' },
toValue() { return 'value-2'},
};
let pt = new PointTwo();
pt.name; // PointOne
pt.toStr(); // Uncaught TypeError: pt.toStr is not a function
pt.toValue(); // pt.toValue is not a function
pt.toName(); // pt.toName is not a function

可以看到 外部添加无效而 为啥有 constructor呢,这是因为constructor方法是class类自带的。

也就是说当要给class类原型添加方法时,如果使用ES5的添加做法并不会生效;当然也不是写在外部就会失效,通过Object.assign方法还是可以做到这一点.

1
2
3
4
5
6
Object.assign(PointTwo.prototype, {
toValue() {return 'value-2'},
'num': 123,
});
pt.toValue(); // value-2
pt.num; // 123

属性的写法

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。(不建议如此做:使用实例的prototype,定义属性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Point {
x = 10;
constructor() {
this.y = 0;
};
showX() {
this.x += 5;
console.log(this.x)
}
showY() {
console.log(this.y)
}
}
let p = new Point();
p.x; //10
p.y; //0
p.showX(); //15
p.showY(); //0

可以看到 定义在 constructor,或 定义在类的最顶层。都是一样的。只是 x 和 constructor 处于同一个层级。这时,不需要在实例属性前面加上this。

这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。

私有变量

关于 class ,JS没有规定私有变量和私有的方法,全是公共的。

虽然有提案 使用 # 来作为私有修饰符,但距离正式的使用可能还有一年。

chrome 80 可以使用 私有属性,使用方法还不行

1
2
3
4
5
6
7
8
class PointOne {
#x = 10; // 私有变量
#y = 5;
constructor() {}
#say() { return #x + #y } // 私有方法
toString () {return 'string-1'}
toValue() {return 'value-1'}
};

所以采取一些方法来产生私有变量

  1. JS界以一种不成文的规定,在变量前加上下划线”_”前缀,约定这是一个私有属性;但实际上,它仍然是一个穿上皇帝新衣般的公共属性。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class A {
    constructor(x) {
    // _x 是一个私有变量
    this._x = x
    }

    getX () {
    return this._x;
    }
    }

    let a = new A(1)

    // _x 依然可以被使用
    a._x // 1
    a.getX() //1
  2. 私有属性不应该出现在原型上,给子类访问。所以只能在类的构造函数或方法中创建。所以使用闭包,来保存上下文,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class A {
    constructor (x) {
    let _x = x;
    this.showX = function () {
    return _x
    };
    }
    }

    let a = new A(1)
    // 无法访问
    a._x // undefined
    // 可以访问
    a.showX() // 1
    私有变量不能直接定义在 prototype 上,所以只能定义在构造函数中,也就是实例上。这导致了构造函数中包含了方法,较为臃肿,对后续维护造成了一定的麻烦。

所以使用 IIFE (立即执行函数表达式) 建立一个闭包,把变量放在 IIFE 中,再定义类,通过 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
26
// class 本质
const A = (function() {
// 定义私有变量_x
let _x;

class A {
constructor (x) {
// 初始化私有变量_x
_x = x
}

showX () {
return _x
}
}

return A;

})()

let a = new A(1)

// 无法访问
a._x // undefined
// 可以访问
a.showX() //1

取值函数(getter)和存值函数(setter)

与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。不能 是 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
26
27
class Point {
name = 'point';
y = 1;
get name() {
console.log('ec');
return 'echo'
};
set name(val) {
console.log('<'+val+'>');
};
get x() {
this.y += 1;
console.log('get')
return this.y;
};
set x(val) {
console.log('set')
this.y += val;
};
};
let s = new Point();
// 不起作用
s.name;// point
s.name = 'book'; // book

s.x;// get
s.x = 5; // set

静态方法 和静态 属性

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

1
2
3
4
5
class Foo {
}

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

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

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

chrome 80 和 node 12 都可以使用 静态属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass {
static myStaticProp = 42;

constructor() {
console.log(MyClass.myStaticProp); // 42
console.log(this.myStaticProp); // undefiend
}
static show() {
console.log(MyClass.myStaticProp); // 42
console.log(this.myStaticProp); // undefiend
}
}
let m = new MyClass();

m.myStaticProp; //undefined
m.show(); //Uncaught TypeError: mms.show is not a function

MyClass.myStaticProp; //42
MyClass.show(); // 42 undefiend

类 继承

Class可以通过extends关键字继承,而非ES5里只能修改原型链(Object.creat(父实例))进行。

子类一定要在constructor方法里调用super方法,执行父类的constructor。这是因为在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。而且子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

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

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

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

第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。如上面所示。

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

作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

1
2
3
4
5
6
7
class A {}

class B extends A {
m() {
super(); // 报错
}
}

第二种情况,super作为对象时,在普通方法中,指向父类的原型对象(即 父类.prototype),在静态方法中,指向父类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()。

在静态方法中,指向父类。最后,父类的静态方法,也会被子类继承。

1
2
3
4
5
6
7
8
9
10
class A {
static hello() {
console.log('hello world');
}
}

class B extends A {
}

B.hello() // hello world

上面代码中,hello()是A类的静态方法,B继承A,也继承了A的静态方法。

引用和转载

es6入门5–class类的基本用法
Class 的基本语法