ES6
的Class
很多大部分的功能,ES5
都是可以实现的, 但是为什么还有出Class
呢? 这是因为ES5
中生成实例对象的方法是通过构造函数生成的。 这与我们所接触的很多语言(C++)差异很大。于是ES6
就提供了跟传统语言的写法。可以说Class
是一个语法糖。
类与ES5
ES6
的Class
与ES5
的构造函数有什么不一样呢?
我们先看ES5
如何生成实例:
1 | function People(name, age) { |
上面代码我们有一个构造函数People
;this
关键字代表实例对象,实例对象上有两个属性(name
, age
) 在原型有两个方法(getName
, getAge
)。通过new
的方式生成实例对象people
;
我们把上面的代码改成用class
来写:
1 | // 改成class |
上面代码我们定义来看一个 类叫People
;里面的constructor
方法就是构造方法、this
表示实例对象(也就是new People
返回的对象)的本身。People
类里有 getName
,getAge
原型方法。类里面的方法都是在类的原型上创建的我们在类里面写方法的时候是不需要写function
关键字;并且方法与方法之间是不需要逗号分隔的,加了反而会报错
总归来说, ES6
的类就是构造函数的另外一种写法。为什么这么说呢?我们对比一下ES5
:
类的数据类型就是函数。类本身就指向构造函数
1
2
3
4
5
6
7
8
9// ES5
function People() {};
console.log(typeof People); // function
console.log(People.prototype.constructor === People); // true
// ES6
class People{}
console.log(typeof People); // function
console.log(People.prototype.constructor === People); // true使用类的时候都是直接通过
new
的命令来使用1
2
3
4
5
6
7// ES5
function People() {};
const people = new People();
// ES6
class People{}
const people = new People();ES6
的类也有在prototype
属性。类中定义的所有方法都是定义在类的prototype
属性上面;首先我们先来了解
ES5
的一些概念:构造函数:
js规定,每个构造函数都有一个
prototype
属性,指向另外一个对象(我们也叫原型对象或者prototype对象
),这个对象的所有属性和方法都会被构造函数所拥有,我们通常把不变的方法方法定义在prototype对象
上,这样构造函数生成的实例对象就可以共享这些方法。实例对象:
构造函数生成的实例对象有一个属性
__proto__
,指向构造函数的原型对象;所有构造函数生成的实例对象的__proto__
与构造函数的prototype
是等价的;实例对象方法和属性的查找规则:先在对象自己身上找,没有再去构造函数的原型对象上查找,还是没有就继续沿着原型链查找。
回到正题:我们
ES6
的类
也是有如ES5
这么一套逻辑的(即有自己的prototype属性)。类中定义的方法都是定义在类的prototype对象上
的。1
2
3
4
5
6
7
8
9class People{
constructor(){}
getName() {}
getAge() {}
}
// 等价
People.prototype.constructor = function(){}
People.prototype.getName = function(){}
People.prototype.getAge = function(){}因此我们类生成的实例上调用的方法其实都是调用
prototype
对象上的方法。1
2
3
4
5
6
7
8
9class People{
constructor(){}
getName() {}
getAge() {}
}
const people = new People();
console.log(people.constructor === People.prototype.constructor); // true
console.log(people.getName === People.prototype.getName); // true
console.log(people.getAge === People.prototype.getAge); // true
但是ES6
的类与ES5
的构造函数还是有不一样的地方的,我们看
类中定义的方法都是不可枚举的
1
2
3
4
5
6
7
8
9
10
11
12
13
14class People {
constructor(name, age) {
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
getAge() {
return this.age;
}
}
Object.keys(People.prototype); // []
Object.getOwnPropertyNames(People.prototype); // [ 'constructor', 'getName', 'getAge' ]可以看出不可枚举。但是
ES5
写的构造函数定义的方法是可以枚举的1
2
3
4
5
6
7
8
9
10
11
12
13
function People(name, age) {
this.name = name;
this.age = age;
}
People.prototype.getName = function() {
return this.name;
}
People.prototype.getAge = function() {
return this.age;
}
Object.keys(People.prototype); // [ 'getName', 'getAge' ]
Object.getOwnPropertyNames(People.prototype); // [ 'constructor', 'getName', 'getAge' ]
constructor
我们在定义类的时候, 必须有constructor
方法,constructor
方法是默认方法,如果没有显式的定义,一个空的constructor
方法会默认添加。
1 | class People {} |
上面代码中我们定义了一个空类 People
,js引擎会自动给它添加constructor
方法。
使用new
生成对象的时候, 会自动调用该方法。constructor
方法默认返回实例对象(即this
)
1 | class People{ |
我们使用new
命令生成实例的时候,constructor
方法会被调用。
类使用的时候必须使用new 调用,否则会报错。
1 | class People{ |
但是普通构造函数式可以不用new
的。
1 | function People(name, age) { |
类的实例
生成一个类的实例,也是使用一个new
命令。如果不用,将会报错(上一节说过);
1 | class People { |
在一个类中, 属性除非是定义在本身(即定义在this对象上)。否则都是定义在原型上。
1 | class People { |
这代码我们定义了一个类叫People
,属性name
,age
实例对象people
的自身属性(因为定义在this
上), 所有hasOwnProperty()
方法返回了true
。而getName
,getAge
是原型对象的属性(因为定义在类上)。所以hasOwnProperty()
返回false
。
跟上面讲过的一样,类的实例共享一个原型对象。
1 | class People { |
属性表达式
类的属性名可以采用表达式,和ES5
也是一样的
1 | // 方法名变量(表达式) |
上面代码中,People
类的方法名getName
,是从表达式得到的。
取值函数(getter)和存值函数(setter)
在类的内部可以使用get
,set
关键字。对一个属性进行设置值和取值。
1 |
|
注意: 我们存取值函数的函数名和里面的变量名不能一致。如:
1 | class People { |
这样写是会报错的。这是因为,在构造函数中执行this.name=name
的时候,就会去调用set name
,在set name方法中,我们又执行this.name = name
,进行无限递归,最后导致栈溢出(RangeError)。
Class 表达式
类也可以使用表达式的方式定义。
1 |
|
如上,我们使用表达式的方式定义了一个类。这个类的名字是MyPeople
。但是MyPeople
只能在类的内部使用,指向的是当前类。在类的外部只能使用People
引用。
当然, 如果内部不用, 可以省略类名。
1 | const People = class { |
采用表达式定义类, 可以写出立即执行的Class
。
1 | const people = new class { |
Class 注意的点
严格模式
类和模块的内部默认就是严格模式,所以我们不需要使用use strict
指定运行模式。只要你的代码写在类和模块当中。就只有严格模式可以使用。
不存在提升
类是不存在提升的(这和ES5
完全是不一样的)
1 | // 不存在声明提升, 在一个类定义好之前使用,直接报错 |
上面的代码 People
类使用在前,定义在后,这样使用是会报错的。因为ES6
不会把类的声明提升到代码块的顶部的。那为什么有这样的规定呢? 这是有原因:这和继承有关,必须保证子类在父类之后定义;
1 | let People = class {}; |
上面的代码不会出错,因为Mypeople
在继承People
的时候,People
已经定义了。但是如果存在类的声明提升,上面的代码就会出错了,这是因为class
会被提升到代码块的顶部, 但是let
不会提升,就会导致Mypeople
在继承People
的时候。People
没有定义。
name 属性
name
属性总是返回紧跟在class
关键字后面的类名。
1 | class People {}; |
this 指向
类的达内部如果有this
,它默认指向类的实例,但是单独使用该方法的时候,就可能会报错。
1 | class People { |
类People
的方法getName
中的this
默认指向People
的实例。但是这个方法提取出来单独调用,this
会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined
),从而导致找不到name属性而报错。
解决的办法可以使用箭头函数。如下这么写:
1 | class People { |
箭头函数内部的this
在定义的时候就确定好了,箭头函数的this
总是指向外层第一个普通函数的this
。上面代码中,箭头函数构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this
会总是指向实例对象。关于this指向的问题可以看这篇文章
静态方法
类相当于实例的原型,所有在类中定义的方法,都会被继承,如果我们不想让一个方法被继承,可以在方法名前加static
关键字,该方法就就不会被实例继承,只能通过类直接调用。
1 | class People { |
上面定义了一个类People
, 类People
里面定义了一个静态方法getName
, 该方法只能类People直接调用
,实例people
调用的话直接报错(该方法不存在)
注意: 如果在静态方法中有this
关键字,这个this
指向类,而不是实例。
1 | class People { |
静态方法可以与非静态方法重名
1 | class People { |
父类的静态方法是可以被子类继承的
1 | class People { |
实例属性的新写法
实例属性除了可以定义在constructor
方法中的this
上,也可以直接定义在类的最顶层。这样的写法看上去比较整齐
1 | class People { |
静态属性
静态属性是指 Class
本身的属性.即Class.propName
,而不是定义在对象(this
)的属性。
1 | class People { |
这表示给类People
添加了静态属性age
。只能People.age
调用。 在this
是取不到的(people.getAge(); // undefined
)
第二种方式,在实例属性前面添加static
关键字
1 | class People { |
类的继承
类可以通过extends
关键字实现继承。比ES5 那么多继承方式方便的多。
1 | class People { |
这段代码定义了一个类 MyPeople
, 通过关键字extends
继承了 People
类的所有属性和方法。我们看到``MyPeople的构造函数和
getName使用了
super。那
super`这代表什么呢? 我们之后下面有讲到。
需要注意: 在子类的construtor
中必须是用super
方法。否则在新建实例的时候会报错。为什么呢?这是因为子类的this
对象必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法。如果不调用super
方法的话,子类就将得不到this
对象。
1 | class People { |
这代码中,子类MyPeople
继承了父类People
, 但是在它的构造函数中没有调用super
方法,导致在新建实例的时候报错。
必须先调用super方法的原因:
ES5的继承方式:先创造子类的实例对象的this
。然后再把父类的属性和方法加到this上面来。
ES6的继承方式:先将父类实例的属性和方法加到this上面来(所以必须先调用super方法)。然后再用子类的构造函数修改this
跟定义类一样,如果子类没有定义construtor方法,那么这个方法就会被默认加上。
1 | class MyPeople extends People {} |
在子类的构造函数中, 只有调用了 super
方法之后,才可以使用this关键字,不然就会报错。这是因为子类的实例的创建基于父类实例,只有`super方法才能调用父类实例。
1 | class People { |
上面的代码在子类MyPeople
的constructor
方法中在使用super()
之前是用了this
。结果在创建实例的时候报错。
上面有讲过父类的静态方法也会被子类继承
1 | class People { |
Object.getPrototypeOf()
Object.getPrototypeOf()
该方法用于从子类获取父类。
1 | class People { |
因此,可以使用这个方法判断,一个类是否继承了另一个类。
super 关键字
super关键字可以当函数使用,也可以当对象使用。这里可以分成下面这几类来:
super作为函数使用
super
作为函数使用时,代表父类的构造函数,ES6要求,子类的构造函数必须执行一次super函数;
1 | class People { |
这段代码子类MyPeople
的constructor
函数中,**super
作为函数调用,代表父类的构造函数**。
注意:作为函数使用, 只能在子类的构造函数中使用,在其他地方使用的话是会报错的。
1 | class People { |
super作为对象在普通方法中使用是指向父类的原型对象
1 | class People { |
super
作为对象在普通方法中使用是指向父类的原型对象(prototype
对象)。
注意:由于这种情况,super
指向父类的原型对象,所以定义在父类实例上的属或者方法是没有办法通过super
调用的
1 |
|
调用super.name
返回undefined
。
如果是定义在父类的原型上就可以取到了。
1 | class People { |
super作为对象在普通方法中调用父类原型的方法,该方法内部的this指向子类的实例(this)
这句话看着好大,但是我们可以分析简单理解下。父类的方法都是定义在原型对象上面,而super作为对象在普通方法中使用时指向父类的原型,所以不用说的那么复杂,可以说成super作为对象在普通函数中调用方法,改方法内部的this指向子类的实例(即this)
1 | class People { |
上面代码中, super.getName();
虽然调用的是People.prototype.getName()
。 但是People.prototype.getName()
中的this
指向的是子类MyPeople
的实例。 导致输出的是”子类实例属性” 而不是“父类实例属性”
super作为对象在静态方法中是指向父类。
1 |
|
super作为对象在子类的静态方法使用,super指向父类(People),而不是父类的原型(People.prototype)。
super作为对象在静态方法中调用父类的方法时,方法内部的this
指向当前的子类,而不是子类的实例。
1 | class People { |
super作为对象在子类的静态方法调用( super.getName();
)这里的super.getName() === People.getName()
。虽然调用的是People.getName()
但是方法中的this指向的是子类(MyPeople
).所以this.age === MyPeople.age
结果为:“子类属性”
使用super设置属性的话,那么super就是当前的this。
1 | class People { |
学习都是来自[ECMAScript 6 入门–Class。