简介
ES6
的 class
可以看作只是一个语法糖,它的绝大部分功能,ES5
都可以做到,新的 class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
简单的对比
// ES5
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
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
let p = new Point(1, 2)
上述的代码分别定义了在 ES5
和 ES6
中类声明的方式,可以看出有几点区别:
ES5
用function
声明类,ES6
使用class
声明。ES5
中函数中的内容移到了ES6/constructor
中。ES5
中在prototype
上配置原型链上的方法,在ES6
中直接定义。
由上可以看出,ES6
中的 class
一定程度上可以认为是 ES5
类写法的语法糖。
接着思考下 ES5
中类是如何实例化的:
- 直接调用类的函数,然后把
this
对象给返回出来。 - 把
prototype
上的内容定义在实例的原型链上。
首先我们来看第一点,在 ES5
的类编写过程中,总会有这么一句
Point.prototype.constructor === Point // true
这说明类 prototype
上的 constructor
属性是自己。
那现在在来看实例化过程中第一步,可以认为调用类下 prototype
上的 constructor
方法。
那么是否可以这么认为:一个类的实例化过程其实和类本身没多大关系了,仅仅和 prototype
有关。
接着在来看 ES6
中的构造方式,我们定义了一个名字和一系列的方法,这其实就是在定义一个类的 prototype
。
但有一点需要尤其注意:
- 在
ES5
中在prototype
上定义的方法是可枚举的,ES6
中定义的所有方法都是不可枚举的。
类的属性名,可以用 []
表示:
let methodName = 'getArea';
class Square {
constructor(length) {
// ...
}
[methodName]() {
// ...
}
}
constructor 方法
constructor
方法是类的默认方法,通过 new
命令生成对象实例时,会调用该方法。一个类必须有 constructor
方法,如果没有显式定义,一个空的 constructor
方法会被默认添加。
class Point {
}
// 等同于
class Point {
constructor() {}
}
constructor
方法默认返回实例对象(this
),当然也可以指定对象返回,但是这就会照成实例对象并不是类的实例。
class Foo {
constructor() {
return Object.create(null)
}
}
new Foo() instanceof Foo
// false
Class 表达式
Class
表达式的效果与函数一致
const MyClass = class Me {
getClassName() {
return Me.name
}
};
let inst = new MyClass();
inst.getClassName(); // Me
Me.name; // ReferenceError: Me is not defined
如果一个类被定义成表达式的形式,那么跟在 class
关键字后的类名在类外部不可见,仅仅只能在类内部使用,如果内部不使用的话,可以去掉。
采用 Class
表达式,可以写出立即执行的 Class
。
let person = new class {
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name)
}
}('张三')
person.sayName(); // "张三"
但感觉这很没必要,这还不如直接写一个对象。
不存在变量提升
类在 ES6
中的定义方式与 let
变量一致,不存在变量提升。
new Foo(); // ReferenceError
class Foo {};
this 指向
在 ES6
实例对象下的方法表现与 ES5
一致,在方法中使用 this
对象需要小心,this
代指拥有这个方法的对象。
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName();
// TypeError: Cannot read property 'print' of undefined
上述写法会报错,原因在于声明的 printName
变量的拥有者是 window
。这与函数下的 this
对象的绑定一致,并不存在说 ES6
下有特殊情况,解决方式和函数确定 this
指向方式一致。
- 使用
bind
。 - 使用箭头函数。
不过使用这两种方法,都需要在 this
对象上绑定同名的原型链上的方法:
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
}
}
// ...
}
Class get/set
与 ES5
一样,在 类 的内部可以使用 get
和 set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: ' + value);
}
}
let inst = new MyClass();
inst.prop = 123; // setter: 123
inst.prop; // 'getter'
这其实也是在语法糖内的简单实现,ES5
中通过 defineProperty
定义 prototype
可以实现一样的效果。
Class 的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例实现。如果在一个方法前,加上 static
关键字,就表示该方法不会被实例实现,而是直接通过类来调用,这就称为 静态方法。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod(); // 'hello'
var foo = new Foo();
foo.classMethod();
// TypeError: foo.classMethod is not a function
当然如果在静态方法包含 this
关键字,这个 this
指的是类,而不是实例,这其实也是遵循函数执行时的 this
指向问题,总是执行调用这个函数的对象,而类仅仅是一个特殊的对象。
父类的静态方法,可以被子类继承。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod(); // 'hello'
但目前 ES6
规定,类下仅仅只能有静态方法,不能有静态属性。