ES6 中的关键字class
本质上还是通过原型链和函数实现的,提供了一种更加方便、逻辑更加清晰的方法去实现类。
所以ES6的class
属于一种语法糖,使写法更优雅,更加面向对象的编程,其思想和ES5没有本质区别。
ES6的
class
内部是基于寄生组合式继承,它是目前最理想的继承方式。
//ES6
class Point{
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
return `${this.x},${this.y}`
}
}
等同于
// ES5
function Point(x,y){
this.x = x;
this.y = y;
}
Point.prototype.toString = function(){
return '(' + this.x + ',' + this.y + ')';
};
constructor
constructor
方法是类的构造函数,是一个默认方法,通过new
命令创建对象实例时,自动调用该方法。
一个类必须有constructor
方法,如果没有显示定义,一个默认的constructor
方法会被默认添加。一般constructor
方法返回实例对象this
,但是也可以指定constructor
方法返回一个全新的对象,让返回的实例对象不是该类的实例。
ES6中的constructor
构造方法对应ES5中的构造函数。
super
super
关键字既可以当函数使用,也可以当对象使用。这两种情况下,它的使用方法完全不同。
对象函数中的this
指向的是当前函数所在的对象,而super
指向的是当前函数所在对象的原型,比this
更深了一层。在子类构造函数中调用super()
,相当于调用父类的constructor
。
super当函数使用
class A{} class B extends A{ constructor(){ super(); // ES6要求,子类的构造函数必须执行一次super函数,否则会报错 }
注:在
constructor
中必须调用super
方法,因为子类没有自己的this
对象,而是继承父类的this
对象,然后对其进行加工,而super
就代表了父类的构造函数。super
虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即super
内部的this
指的是 B,因此super()
在这里相当于A.prototype.constructor.call(this, props)
class A {
constructor() {
console.log(new.target.name); // new.target 指向当前正在执行的函数
}
}
class B extends A {
constructor() {
super();
}
}
new A(); // A
new B(); // B
可以看到,在
super()
执行时,它指向的是 子类 B 的构造函数,而不是父类 A 的构造函数。也就是说,super()
内部的this
指向的是 B。
super当对象使用
- 在普通方法中,指向父类的原型对象;
- 在静态方法中,指向父类。
class A{ c(){ return 2 } } class B extends A{ constructor(){ super(); console.log(super.c()); // 2 } } let b = new B();
上面代码中,子类B当中的
super.c()
就是将super
当作一个对象使用。这时,super
在普通方法之中,指向A.prototype
,所以super.c()
就相当于A.prototype.c()
通过
super
调用父类的方法时,super
会绑定子类的this
class A{ constructor(){ this.x = 1; } s(){ console.log(this.x) } } class B extends A{ constructor(){ super(); this.x = 2; } m(){ super.s() } } let b = new B(); b.m(); // 2
上面代码中,
super.s()
虽然调用的是A.prototytpe.s()
,但是A.prototytpe.s()
会绑定子类 B 的this
,导致输出的是 2,而不是 1。也就是说,实际上执行的是super.s.call(this)
。
由于绑定子类的 this
,所以如果通过 super
对某个属性赋值,这时 super
就是 this
,赋值的属性会变成子类实例的属性。
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();
上面代码中,
super.x
赋值为 3,这时等同于对this.x
赋值为 3。而当读取super.x
的时候,调用的是A.prototype.x
,但并没有 x 方法,所以返回undefined
。
注意,使用 super
的时候,必须显式指定是作为函数,还是作为对象使用,否则会报错。
class A {}
class B extends A {
constructor() {
super();
console.log(super); // 报错
}
}
上面代码中,
console.log(super);
中的super
,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。如果能清晰的表明super
的数据类型,就不会报错。
由于对象总是继承其他对象的,所以可以在任意一个对象中,使用 super 关键字。
静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上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指的是类,而不是实例
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar() // hello
上面代码中,静态方法
bar
调用了this.baz
,这里的this
指的是Foo
类,而不是Foo
的实例,等同于调用Foo.baz
。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。
- 父类的静态方法,可以被子类继承。
- 静态方法也是可以从
super
对象上调用的。
私有方法和私有属性
私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。
一种做法是在命名上加以区别
class Widget { // 公有方法 foo (baz) { this._bar(baz); } // 私有方法 _bar(baz) { return this.snaf = baz; } // ... }
另一种方法就是索性将私有方法移出类,因为类内部的所有方法都是对外可见的。
class Widget { foo (baz) { bar.call(this, baz); // 内部调用了bar.call(this, baz)。这使得bar()实际上成为了当前类的私有方法 } // ... } function bar(baz) { return this.snaf = baz; }
还有一种方法是利用
Symbol
值的唯一性,将私有方法的名字命名为一个Symbol
值。
静态块
ES2022引入的新概念。允许在类的内部设置一个代码块,在类生成时运行一次,主要作用是对静态属性进行初始化。
class C {
static x = ...;
static y;
static z;
static {
try {
const obj = doSomethingWith(this.x);
this.y = obj.y;
this.z = obj.z;
}
catch {
this.y = ...;
this.z = ...;
}
}
}
上面代码中,类的内部有一个
static
代码块,这就是静态块。它的好处是将静态属性y和z的初始化逻辑,写入了类的内部,而且只运行一次。
每个类只能有一个静态块,在静态属性声明后运行。静态块的内部不能有return语句。
静态块内部可以使用类名或this,指代当前类。
new.target属性
new
是从构造函数生成实例对象的命令。ES6为new
命令引入了一个new.target
属性,该属性一般用在构造函数之中,返回new
命令作用于的那个构造函数。如果构造函数不是通过new
命令或Reflect.construct()
调用的,new.target
会返回undefined
,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
class
内部调用new.target
,返回当前 class
。
需要注意的是,子类继承父类时,new.target
会返回子类。
利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
ES6与ES5的对比
静态方法对比
- ES6实现方式
class Employee{ constructor(name, dept){ this.name = name; this.dept = dept; } static func(){ console.log('static') } getName(){ console.log(this.name) } }
- ES5实现方式
function Employee(name, dept){ this.name = name; this.dept = dept; } Employee.fun = function(){ console.log("static") } Employee.prototype.getName=function(){ console.log(this.name) }
继承方式对比
- ES6实现方式
class Manager extends Employee{ constructor(name, dept, reports){ super(name, dept); this.reports = reports; } }
- ES5实现方式
function Manager(reports, name ,dept){ Employee.call(this, name, dept); this.reports = reports; } Manager.fun = Employee.fun;
class关键字的一些限制
- 灵活性
- 类声明不可提升
- 无法重写类
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 chaoyumail@126.com