JavaScript类
类实质上是 JavaScript 现有的基于原型继承的语法糖。类的主体是在大括号中间的部分。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return this;
}
}
console.log(typeof Animal); // function
console.log(Animal === Animal.prototype.constructor); // true
上面的代码用es5的方式写:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return this;
}
类声明和类表达式的主体都执行在严格模式下。比如,构造函数,静态方法,原型方法,getter和setter都在严格模式下执行。
类声明
类声明不像函数声明一样会提升,所以要先声明类,然后再使用。
var rectangle = new Rectangle(); // 在类的声明语句之前使用类会报错
class Rectangle {
}
类表达式
类表达式可以是具名的,也可以是匿名的。
具名类表达式
一个具名类表达式的名称是类内的一个局部属性,它可以通过类本身(而不是类实例)的name属性来获取。
// 具名类
let Rectangle = class Rectangle2 {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name);
// 输出: "Rectangle2"
匿名类表达式
// 匿名类
let Rectangle = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name);
// output: "Rectangle"
类体和方法定义
构造函数 constructor
constructor方法是一个特殊的方法,这种方法用于创建和初始化一个由class创建的对象。
一个类只能拥有一个名为 “constructor”的特殊方法。如果类包含多个constructor的方法,则将抛出 一个SyntaxError 。
constructor方法默认返回实例对象(即this),可以手动改写指定返回另外一个对象。
没有显式定义类的constructor方法,会默认为其添加一个空的constructor方法:constructor() {}。
class Animal {
// 没有显示定义Animal的constructor方法,会默认为其添加一个空的constructor方法。
// constructor() {}
speak() {
console.log('Hello world!');
}
}
console.log(Animal === Animal.prototype.constructor); // true
// class Animal {} 实际上对应的是ES5中的 function Animal {} 写法
super
super关键字的规则比较复杂,既可以作为函数使用,也可以作为对象使用。
super([arguments]);
super.functionOnParent([arguments]);
在子类中, 在使用'this'之前, 必须先调用super(), 否则, 将导致引用错误。
class Polygon {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
class Square extends Polygon {
constructor(length) {
// 注意: 在子类中, 在你使用'this'之前, 必须先调用super()。否则, 将导致引用错误。
this.height = lenght; // ReferenceError,super 需要先被调用!
// 这里,调用父类的构造函数,
super(length, length);
}
}
super在子类中使用的位置不一样,可调用的方法也不一样。
- 在子类的构造函数中单独使用时,只能作为父类的构造函数调用:super(),此时其内部的this指向子类的实例
- 在子类的构造函数中或者在子类的原型方法中作为对象使用时,此时super指向父类原型,通过super可以调用父类中的原型方法和访问父类原型上的属性,通过super调用的方法内部的this指向子类的实例(有一点要额外说明在子类构造函数中使用super可以修改子类实例的属性值,可以给子类实例添加属性)
- 在子类的静态方法或者给子类的静态公有字段赋值时,此时super指向父类,通过super可以调用父类的静态方法和静态公有字段,通过super调用的父类静态方法中的this指向子类本身;
使用super给公有实例字段赋值,super指向父类的原型。将在公有实例字段一节中说明。
验证第一条和第二条的例子:
class Polygon {
constructor(height, width) {
this.name = 'Rectangle';
this.height = height || 0;
this.width = width || 0;
return this;
}
sayName() {
console.log(this.name);
}
get area() {
return this.height * this.width;
}
set area(value) {
this._area = value;
}
}
Polygon.prototype.pp = 100; // 定义在父类原型上的属性
class Square extends Polygon {
constructor(length) {
// 这里,调用父类的构造函数,
// 但实质上是通过父类的构造函数给子类的实例添加了name,height,width属性
// 再稍微深入一想,super作为函数使用时,在子类的构造函数中调用,其内部的this指向了子类的实例
console.log(super(length, length)); // 打印出的是子类的实例
this.test = 'test';
this.name = 'Square';
// 访问的是父类原型上的方法,方法内部的this同样指向了子类的实例
super.sayName(); // Square
}
sayName() {
super.sayName(); // Square
console.log(super.pp); // 100 通过super访问父类原型上的属性
console.log(super.constructor === Polygon); // true
}
}
验证第三条的例子:
class Animal {
static test = 'ttt';
constructor(name) {
this.name = name;
return this;
}
static printFather(str) {
console.log('this is father' + '---' + str + '--' + this.name);
}
}
class Dog extends Animal {
static dd = super.test; // 还可以访问父类的静态公有字段 此时super指向父类自身
pp = super.test; // 此时super指向父类的原型
static printSon() {
super.printFather('son'); // 调用父类的静态方法,此时printFather方法中的this指向了Dog类自身
}
constructor(name) {
super(name);
}
}
var ss = new Dog('pubby');
Dog.printSon(); // this is father---son--Dog
console.log(Dog.dd); // ttt
console.log(ss.pp); // undefined
对于第二条的额外说明的验证:
class A {
constructor() {
this.x = 1;
this.z = 12;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
// 根据结果来看,下面两条语句的super实际上是this
super.x = 3;
super.y = 4;
console.log(super.x); // undefined,这里super指向了父类的原型
console.log(this.x); // 3
}
}
let b = new B();
console.log(b.x); // 3
console.log(b.y); // 4
类主体中的严格模式
类主体中的所有的函数、方法、构造函数、getters或setters都在严格模式下执行。
class Animal {
speak() {
return this;
}
static eat() {
return this;
}
}
let obj = new Animal();
obj.speak(); // Animal {}
let speak = obj.speak;
speak(); // undefined speak方法是在严格模式下声明的,单独调用时this不会指向全局对象
Animal.eat() // class Animal
let eat = Animal.eat;
eat(); // undefined eat方法是在严格模式下声明的,单独调用时this不会指向全局对象
实例属性
实例的属性一般定义在this上(也可以使用其他方法定义实例属性,下文会讲到):
class Rectangle {
constructor(height, width) {
this.height = height; // height是实例属性
this.width = width; // width是实例属性
}
test() { // test方法定义在类的原型上(Rectangle.prototype)
console.log('test');
}
}
静态的和原型的数据属性:
Rectangle.staticWidth = 20; // 静态属性 --- 相当于定义在类的构造函数上
Rectangle.prototype.prototypeWidth = 25; // 原型属性 --- 定义在原型上
字段声明
- 公有字段
- 公共方法
- 私有字段
- 私有方法
公有字段
静态公有字段
静态公有字段用关键字static声明。
我们声明的静态公有字段,本质上是使用Object.defineProperty方法添加到类的构造函数上。
在类被声明之后,可以通过类的构造函数访问静态公有字段。
class ClassWithStaticField {
static staticField = 'static field';
// 没有设置值的字段将默认被初始化为undefined。
static staticParam;
}
console.log(ClassWithStaticField.staticField); // 输出值: "static field"
console.log(ClassWithStaticField.staticParam); // undefined
静态公有字段不会在子类里重复初始化,但我们可以通过原型链访问它们。
下面的代码中子类Dog通过原型链访问到了父类Animal上的静态公有字段test。
class Animal {
static test = 'ttt';
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
static tt = 'pp';
constructor(name) {
super(name);
}
}
console.log(Dog.tt); // pp
console.log(Dog.test); // ttt
当初始化字段时,类主体中的this指向的是类的构造函数(前面验证过静态方法中的this也是指向类的构造函数)。
class Animal {
static test = 'ttt';
static testOne = this.test; // 这里this指向Animal
constructor(name) {
this.name = name;
this.test = 'instance';
}
}
class Dog extends Animal {
static tt = 'pp';
constructor(name) {
super(name);
console.log(super.test); // undefined 此时super指向的是父类的原型Animal.prototype
}
}
var dog = new Dog('pubby');
console.log(Dog.testOne); // ttt
公有实例字段
公有实例字段是声明实例属性的另外一种方式。可以在类的构造函数之前声明,也可以在类的构造函数之后声明。
注意,如果在类的构造函数之后声明公有实例字段,必须先在构造函数中调用父类的构造函数 -- super。
class Animal {
vv = 'vv';
}
class Dog extends Animal {
oo = 'oo';
}
var animal = new Animal();
var dog = new Dog();
console.log(dog.oo); // oo
console.log(animal.vv); // oo
// 下面这种写法会报错。必须在构造函数中调用super()后才可以正常运行
class Animal {
vv = 'vv';
}
class Dog extends Animal {
oo = 'oo';
constructor() {}
}
var dog = new Dog();
console.log(dog.oo);
class Animal {
vv = 'vv';
}
class Dog extends Animal {
oo = super.test;
constructor(name) {
super();// super此时是父类的构造函数
}
xxx = 'xxx'; // 如果不先调用super()就声明xxx,将会报错
}
var dog = new Dog();
console.log(dog.xxx); // xxx
公有实例字段如果只声明而不赋值则会初始化为undefined。
class Animal {
vv;
}
var animal = new Animal();
console.log(animal.vv); // undefined
公有实例字段也可以由计算得出:
var str = 'Person';
class Person {
['the' + str] = 'Jack';
}
var person = new Person();
console.log(person.thePerson); // Jack
下面看一个复杂点的例子,注意例子中的this和super:
class Animal {
fatherClass = 'Animal';
copyFatherClass = this.fatherClass;
}
Animal.prototype.childClass = 'i am a child';
class Dog extends Animal {
childClass = super.childClass;
}
var animal = new Animal();
var dog = new Dog();
console.log(animal.copyFatherClass); // Animal
console.log(dog.childClass); // i am a child
使用super给公有实例字段赋值,super指向父类的原型。
公共方法
静态方法
在类中定义的方法都存在于原型上,都会被实例继承。但也有例外。
如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
不能通过一个类的实例调用静态方法。静态方法通常用于为一个应用程序创建工具函数。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static distance(a, b) {
return a * b;
}
}
const p1 = new Point(5, 5);
console.log(Point.distance(10,20)); // 200
console.log(p1.distance(10,20)); // TypeError: p1.distance is not a function
如果静态方法包含this关键字,这个this指的是类本身(构造函数),而不是实例。
class Foo {
static oneFun() {
this.baz(); // 在静态方法内,this指向类本身,而不是类的实例
}
static baz() {
console.log('hello'); // 静态方法可以和实例方法重名,而不会覆盖实例方法
}
baz() {
console.log('world');
}
}
Foo.oneFun() // hello
父类的静态方法可以被子类继承:
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
公共实例方法
class Animal {
say(str) {
return str;
}
}
上面代码中的say方法就是一个公共实例方法。
公共实例方法是定义在类的原型上的。
getter和setter
getter和setter是和类的属性绑定的特殊方法,分别会在其绑定的属性被取值、赋值时调用。
使用get和set句法定义实例的公共getter和setter。
// MDN上的例子
class ClassWithGetSet {
#msg = 'hello world'; // 这是一个私有字段
get msg() {
return this.#msg;
}
set msg(x) {
this.#msg = `hello ${x}`;
}
}
const instance = new ClassWithGetSet();
console.log(instance.msg); // hello world
instance.msg = 'cake';
console.log(instance.msg); // hello cake
console.log(instance.hasOwnProperty('msg')); // false
下面的例子是《ECMAScript 6 入门》里的,解释了类中getter和setter的本质:
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(
CustomHTMLElement.prototype, "html"
);
"get" in descriptor // true
"set" in descriptor // true
公共实例方法也可以是一个Generator函数。
私有字段
声明私有字段的时候需要加上#前缀。
例如:
#test是一个私有字段,#与后面的test是一个整体,使用的时候要写完整。
也可就是说#test与test是完全不同的两个字段。
静态私有字段
静态私有字段需要使用static关键字声明;
class Animal {
static #test = 'doom';
ppp() {
return Animal.#test;
}
}
var animal = new Animal();
console.log(animal.ppp()); // 'doom'
静态私有字段必须在声明他的类的内部使用(在哪个类内部声明,在哪个类的内部使用,不参与继承):
// 虽然子类继承了父类的静态方法,但是并没有继承父类的静态私有属性,也不能使用他
class BaseClassWithPrivateStaticField {
static #PRIVATE_STATIC_FIELD;
static basePublicStaticMethod() {
this.#PRIVATE_STATIC_FIELD = 42;
return this.#PRIVATE_STATIC_FIELD;
}
}
class SubClass extends BaseClassWithPrivateStaticField { };
console.log(SubClass.basePublicStaticMethod());// 报错:Cannot write private member #PRIVATE_STATIC_FIELD to an object whose class did not declare it
私有实例字段
声明私有实例字段要使用#前缀,不使用static关键字。
私有实例字段也只能在类的内部使用。
class Animal {
#test = 'test';
say() {
console.log(this.#test);
}
}
var animal = new Animal();
animal.say();
console.log(animal.#test); // SyntaxError
私有方法
私有方法的内容目前还是提案,在node 12版本中还不支持
静态私有方法
静态私有方法使用static关键字进行声明,需要添加#前缀。
静态私有方法与静态方法一样,只能通过类本身调用,并且只能在类的声明主体中使用:
class Animal {
static #test() {
console.log('static #test call');
}
ttt() {
Animal.#test();
}
}
var animal = new Animal();
animal.ttt();
私有实例方法
私有实例方法在类的实例中可用,它的访问方式的限制和私有实例字段相同。
class ClassWithPrivateMethod {
#privateMethod() {
return 'hello world';
}
getPrivateMessage() {
return #privateMethod();
}
}
const instance = new ClassWithPrivateMethod();
console.log(instance.getPrivateMessage());
// 预期输出值: "hello world"
私有实例方法可以是生成器、异步或者异步生成器函数。私有getter和setter也是可以的:
class ClassWithPrivateAccessor {
#message;
get #decoratedMessage() {
return `✨${this.#message}✨`;
}
set #decoratedMessage(msg) {
this.#message = msg;
}
constructor() {
this.#decoratedMessage = 'hello world';
console.log(this.#decoratedMessage);
}
}
new ClassWithPrivateAccessor();
// 预期输出值: "✨hello world✨"