JS类与类继承
2023-09-25 23:36:56 #JS

class 的基本语法

关于 class

  • ES6 引用了关键字 class 定义“类”,但是底层仍然是基于原型实现继承的方式,class 只是语法糖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ES6 class 定义类并生成新对象
class Point {
// 构造方法
constructor(x, y) {
this.x = x;
this.y = y;
}
// 成员方法
toString() {
return `(${this.x}, ${this.y})`;
}
}

const point = new Point(3, 4);
point.toString(); // (3, 4)
1
2
3
4
5
6
7
8
9
10
11
12
// ES5 构造函数定义并生成新对象
function Point(x, y) {
this.x = x;
this.y = y;
}

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

var point = new Point(1, 2);
point.toString(); // (1, 2)
  • 类的数据类型就是函数,类本身就指向构造函数
1
2
typeof Point                             // function
Point === Point.prototype.constructor // true
  • 类的所有方法都定义在类的 prototype 属性上
1
2
3
4
5
6
7
8
9
10
11
class Point {
constructor(){}
toString(){}
toValue(){}
}
// 等同于
Point.prototype = {
constructor(){},
toString(){},
toValue(){},
};
  • 在类的实例上调用方法,其实就是调用原型上的方法
1
2
3
4
class A = {}
let a = new A();

a.constructor === A.prototype.constructor // true
  • 类的新方法可以添加在 prototype 对象上,Object.assign 方法可以一次向类添加多个方法
1
2
3
4
5
6
7
8
9
10
class Point {
constructor() {
// ...
}
}

Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
  • 类的内部定义的所有方法都是不可枚举的
1
2
3
4
5
6
7
8
9
10
11
12
class Point {
constructor(x, y) {
// ...
}

toString() {
// ...
}
}

Object.keys[Point.prototype] // undefined
Object.getOwnPropertyNames(Point.prototype) // ['constructor', 'toString']
  • 类的属性名可以采取表达式
1
2
3
4
5
6
7
8
9
10
11
let methodName = 'getArea';

class Square {
constructor(length) {
// ...
}

[methodName]() {
// ...
}
}

constructor 方法

  • constructor 方法是类的默认方法,通过 new 命令生成对象实例时自动调用该方法
  • 一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加
1
2
3
4
5
6
class Point {
}
// 等同于
class Point {
constructor() {}
}
  • constructor 方法默认返回实例对象(即 this),也可以指定返回另外一个对象
1
2
3
4
5
6
class Point {
constructor() {
return Object.create(null); // 返回一个新的对象
}
}
new Point instanceof Point // false
  • 类必须使用 new 来调用,否则会报错
1
2
3
4
5
6
7
class Point {
constructor(x) {
this.x = 1;
}
}

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

类的实例对象

  • 实例的属性除非显式定义在其本身(即 this 对象)上,否则都是定义在原型(即 class)上
1
2
3
4
point.hasOwnProperty('x');                     // true 
point.hasOwnProperty('y'); // true
point.hasOwnProperty('toString'); // false
point.__proto__.hasOwnProperty('toString'); // true
  • 类的所有实例共享一个原型对象
1
2
3
const p1 = new Point(1, 2);
const p2 = new Point(3, 4);
p1.__proto__ === p2.__proto__ // true

class 表达式

  • class 可以使用表达式的形式定义
1
2
3
4
5
6
7
8
9
10
// 类的名字是 Myclass 而不是 Me, Me 只在 class 的内部代码有用
const Myclass = class Me {
getclassName() {
return Me.name; // name 属性总是返回紧跟在 class 关键字后面的类名
}
};

let inst = new Myclass();
inst.getclassName(); // Me
Me.name // Me is not defined

不存在变量提升

  • 类不存在变量提升,必须保证子类在父类之后定义
1
2
new Point();          // ReferenceError
class Point {}

this 的指向

  • 类的方法内部的 this 默认指向类的实例,如果将该方法提取出来单独使用,this 会指向该方法运行时所在的环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`); // this 默认指向 Logger 类的实例
}

print(text) {
console.log('text: ', text);
}
}

const logger = new Logger();
logger.printName(); // text: Hello there
const {printName} = logger;
printName(); // TypeError:this 指向了 window 对象
  • 对于上述问题解决方案1:在构造函数中绑定 this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}

printName(name = 'there') {
this.print(`Hello ${name}`);
}

print(text) {
console.log('text: ', text);
}
}

const logger = new Logger();
const {printName} = logger;
printName(); // text: Hello there
  • 对于上述问题解决方案2:使用箭头函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
}
}

print(text) {
console.log('text: ', text);
}
}

const logger = new Logger();
const {printName} = logger;
printName(); // text: Hello there

class 的继承

关于 class 继承

  • class 通过 extends 关键字实现继承
  • 子类没有自己的 this 对象,而是继承父类的 this 对象
  • ES6 的继承机制实质是,先创造父类的实例对象 this,然后再用子类的构造函数修改 this
  • 在子类的构造函数中,只有调用 super 之后才可以使用 this 对象,否则会报错,因为子类实例的构建是基于对父类实例加工,只有 super 方法才能返回父类实例
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 {
constructor(x, y) {
this.x = x;
this.y = y;
}

toString() {
return `(${this.x}, ${this.y})`;
}
}

class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}

toString() {
return `${this.color} ${super.toString()}`;
}
}

const cp = new ColorPoint(2, 3, 'blue');
cp.toString(); // blue (2,3)

console.log(cp instanceof Point); // true
console.log(cp instanceof ColorPoint); // true

getPrototypeOf() 方法

  • Object.getPrototypeOf() 方法可以用来从子类上获取父类,因此可以使用这个方法判断一个类是否继承了另一个类
1
Object.getPrototypeOf(ColorPoint) === Point		// true

super 关键字

  • super 关键字既可以当作函数使用,也可以当作对象使用,使用 super 的时候,必须显式指定是作为函数还是作为对象使用,否则会报错
1
2
3
4
5
6
7
class A {}
class B extends A {
constructor() {
super();
console.log(super); // SyntaxError: 'super' keyword unexpected here
}
}
  • 由于对象总是继承其他对象的,所以可以在任意一个对象中使用 super 关键字
1
2
3
4
5
6
let obj = {
toString() {
return 'myObject: ' + super.toString();
}
};
obj.toString(); // 'myObject: [object Object]'

super 作为函数

  • ES6 要求,子类的构造函数必须执行一次 super 函数,否则会报错
1
2
3
4
5
6
7
8
9
class A {}
class B extends A {
constructor() {
// super();
}
}

const b = new B();
console.log(b); // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
  • super 作为函数时,super() 只能用在子类的构造函数中,用在其他地方会报错
1
2
3
4
5
6
class A {}
class B extends A {
m() {
super(); // SyntaxError: 'super' keyword unexpected here
}
}
  • super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指向的是 B
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
constructor() {
console.log(new.target.name); // new.target 指向当前正在执行的函数
}
}

class B extends A {
constructor() {
super(); // super 内部的 this 指向 B,super() 等同于 A.prototype.constructor.call(this)
}
}

new A(); // A
new B(); // B

super 作为对象

  1. 在普通方法中指向父类的原型对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
p() {
return 2;
}
}

class B extends A {
constructor() {
super();
super.p(); // super 作为对象,super.p() 等同于 A.prototype.p()
}
}

const b = new B();
b.p(); // 2
  • 由于 super 指向父类的原型对象,因此,定义在父类实例上的属性不能通过 super 获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
constructor() {
this.p = 2;
}
}

class B extends A {
m() {
return super.p;
}
}

const b = new B();
b.m(); // undefined
  • 定义在父类的原型对象上的属性,可以通过 super 获取
1
2
3
4
5
6
7
8
9
10
11
class A {}
A.prototype.p = 2;

class B extends A {
m() {
return super.p;
}
}

const b = new B();
b.m(); // 2
  • 通过 super 调用父类的方法时,super 会绑定子类的 this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A {
constructor () {
this.x = 1;
}
print() {
console.log(this.x);
}
}

class B extends A {
constructor () {
super();
this.x = 2;
}
m() {
super.print(); // 等同于 A.prototype.print.call(this)
}
}

let b = new B();
b.m(); // 2
  • 如果通过 super 对某个属性赋值,这时 super 就是 this,赋值的属性会变成子类实例的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
constructor() {
this.x = 1;
}
}

class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3; // 等同于 this.x = 3
console.log(super.x); // undefined
console.log(this.x); // 3
}
}

const b = new B();
  1. 在静态方法中指向父类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}

class B extends A {
static myMethod(msg) {
super.myMethod(msg); // super 指向父类
}
myMethod(msg) {
super.myMethod(msg); // super 指向父类的原型对象
}
}

B.myMethod(1); // static 1
const b = new B();
b.myMethod(2); // instance 2

类的 prototype 属性和 __proto__ 属性

  • 每个对象都有 __proto__ 属性,指向对应的构造函数的 prototype 属性
  • class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链
  • 子类的 __proto__ 属性表示构造函数的继承,总是指向父类
  • 子类 prototype 属性的 __proto__ 属性表示方法的继承,总是指向父类的 prototype 属性
1
2
3
4
class A {}
class B extends A {}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

实例的 __proto__ 属性

  • 实例的 __proto__ 属性指向其构造函数的 prototype 属性
  • 子类实例的 __proto__ 属性的 __proto__ 属性指向父类实例的 __proto__ 属性,即子类的原型的原型就是父类的原型
1
2
3
4
5
6
class A {}
class B extends A {}
const a = new A();
const b = new B();
a.__proto__ === A.prototype // true
b.__proto__.__proto__ === a.__proto__ // true

注意!

  • Javascript 中一切皆对象,函数也属于对象
  • 所有对象都含有 __proto__ 属性
  • 只有函数才有 prototype 属性,即函数既有 prototype 属性也有 __proto__ 属性 (原型对象只是对象,不是函数,即原型对象无原型)
  • 所有函数的默认原型都是 Object 的实例
  • 函数都是由 Function 函数生成的(Function 和 Object 是函数也是对象,都是由 Function 函数生成的)
  • 只要是对象,就一定有相应的构造函数(除了 Object 的原型对象)