JavaScript简明教程-class

1,444 阅读9分钟

基本概念

ECMAScript 2015 中引入的 JavaScript 类,实质上是基于原型继承的语法糖,类的语法没有为 JavaScript 引入新的面向对象的继承模型。采用类的写法,只是让原本对象原型的写法更加清晰,更像传统的面向对象语法。

类与函数

  • 类实际上是个“特殊的函数”,类有两种定义方式:类表达式和类声明。
    • 类本身指向构造函数,
    • 类声明不存在函数提升的问题
      • 需要先声明你的类,然后才能访问,不过如果使用类表达式还是会有作用域提升的问题,习惯上会使用类声明的方式
/// 类的表达式
/// 匿名类
let Rectangle = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
...

/// 类声明
class Point {
	constructor (x, y) {
		this.x = x;
		this.y = y;
	}
	toString () {
		return `x:${this.x}, y:${this.y}`;
	}
}
let p = new Point(1, 2);
p.toString();
// x:1, y:2
/// 使用起来和普通函数一致
...

typeof Point;
// function
...

Point === Point.prototype.constructor
// true
  • 类的所有方法都是定义在类的 prototype 属性上
class Point {
	constructor () {}
	toString () {}
}
// 等同于

Point.prototype = {
  constructor() {},
  toString() {}
};
  • 当类的实例在调用方法时,其实就是在调用原型上的方法
p.toString === Point.prototype.toString
// true
  • 类内定义的所有方法都是不可遍历的(查看 Point.prototype 是因为类的所有方法都是定义在原型上)
Object.keys(Point.prototype);
// []
Object.getOwnPropertyNames(Point.prototype);
// ["constructor", "toString"]
...

Object.getOwnPropertyNames(Point)
//  ["length", "prototype", "name"]
...

p.hasOwnProperty('toString'); // false
Reflect.getPrototypeOf(p).hasOwnProperty('toString'); // true
// Reflect.getPrototypeOf(p)获取p的原型相当于Point.prototype

类体和方法定义

一个类的类体是一对花括号/大括号 {} 中的部分。这是你定义类成员的位置,如方法或构造函数。

  • 类声明和类表达式的主体都执行在严格模式下

  • constructor方法是一个特殊的方法,这种方法用于创建和初始化一个由 class 创建的对象。

    • 一个类只能拥有一个名为 constructor 的特殊方法
    • 如果没有显示定义该方法,会自动添加一个空的 constructor 方法
    • 通过 new 生成对象实例时,自动调用该方法
    • 一个构造函数可以使用 super 关键字来调用一个父类的构造函数
    • constructor 方法默认返回实例对象,但可以指定返回另外一个对象
class Foo {
	constructor () {
		return Object.create(null);
	}
}
new Foo() instanceof Foo; 
// false
...
/// 类必须通过new调用,否则会报错
Foo();
// Uncaught TypeError: Class constructor Foo cannot be invoked without 'new'
  • static
    • 如果跟随一个方法,就形成了静态方法
    • 该方法不会被实例继承,而是直接通过类来调用
    • 类实例不能调用静态方法
    • 如果在静态方法中包含 this 关键字,这个 this 指的是类,而不是实例
      • 静态方法也是可以从 super 对象上调用的
    • 父类的静态方法,可以被子类继承
    • 如果跟随一个属性,就形成了静态属性
      • 没有设定初始化值的,会被默认被初始化为 undefined
/// 静态属性
class Point {
    static name = 'point' 
}

Point.name; // point

let p1 = new Point();
p1.name; // undefined
...

/// 静态方法
/// 静态方法和非静态方法可以重名
class Foo {
	static bar () {
		this.baz(); // this 指向的是 Foo,此处相当于调用 Foo.baz()
	}
	static baz () {
		console.log('hello');
	}
	baz () {
		console.log('world');
	}
}
Foo.bar(); 
// hello
...

/// 父类的静态方法和静态属性,可以被子类继承
class Foo {
	static info = 'info'
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.info; // 'info'
Bar.classMethod(); // hello
  • this 指向
    • 当调用静态或原型方法时没有指定 this 的值,那么方法内的 this 值将被置为 undefined
      • 严格模式下不会发生自动装箱,this 值将保留传入状态
      • 在非严格模式下方法调用会发生自动装箱。若初始值是 undefined,this 值会被设为全局对象
    • 实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)
    • 类的方法内部如果含有this,它默认指向类的实例对象
/// 严格模式下不会发生自动装箱,this 值将保留传入状态
function say() {
	'use strict';
	return this;
}
say(); // undefined 严格模式下 this 指向为 undefined
let obj = {say}
obj.say(); // {say: ƒ} 返回调用时的调用对象
...

/// 非严格模式下,this 值会被设为全局对象
function say() {
	return this;
}
say(); // window
...

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

var point = new Point(2, 3);
Reflect.ownKeys(point); // ["x", "y"] constructor 中显示定义在 this 对象上,所以在实例对象上可以找到相关属性
Reflect.ownKeys(point.__proto__); // ["constructor", "toString"] toString 没有直接定义在this对象上,最终是定义在原型上
...

class Logger {
	constructor () {}
	printName (name = 'there') {
		this.print(`Hello ${name}`);
	}
	print (text) {
		console.log(text);
	}
}
const logger = new Logger();
logger.printName(); // printName方法内部的this指向Logger类的实例
// Hello there
...

const { printName } = logger; // 如果这样单独提取出来,this 会指向该方法运行时所在环境,因为找不到 print 方法报错
printName(); // Uncaught TypeError: Cannot read property 'print' of undefined
...

/// 上面的使用方式如果想成立,可以在 constructor 中绑定或者使用箭头函数
class Logger {
	constructor () {
		this.printName = (name = 'there') => {
			this.print(`Hello ${name}`);
		}
	}
	print (text) {
		console.log(text);
	}
}
const logger = new Logger();
const { printName } = logger; 
printName();
  • name属性 总是返回紧跟在class关键字后面的类名
class Me {
  getClassName() {
    return Me.name;
  }
}
new myClass().getClassName();
// Me
  • class取值函数(getter)存值函数(setter)
    • 对某个属性设置存值函数和取值函数,在进行相关操作时会存在拦截该属性的操作行为
class MyClass {
	constructor () {}
	get prop () {
		return 'prop';
	}
	set prop (value) {
		console.log(`setter: ${value}`);
	}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop;
// prop
  • 属性表达式
    • 类的属性名,可以采用表达式
let print = 'printName';
class MyClass {
	constructor () {}
	[print] () {
		console.log('print name')
	}
}
let inst = new MyClass();
inst.printName();
// print name
  • classGenerator
    • 如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数,for...of循环会自动调用这个遍历器
class Foo {
	constructor (...args) {
		this.args = args;
	}
	* [Symbol.iterator] () {
		for (let arg of this.args) {
			yield arg;
		}
	}
}
for (let x of new Foo('Hello', 'world')) {
	console.log(x)
}
// Hello
// world
  • 实例属性
    • 实例属性可以在 constructor 方法里面的 this 上定义,也可以在类的最顶层定义
class Foo {
	constructor (foo) {
		this.foo = foo;
	}
}
new Foo('foo').foo; // foo
...

/// 这个例子中,这么写看起来会有些多余
class Foo {
	foo = '';
	constructor (foo) {
		this.foo = foo;
	}
}
new Foo('foo').foo; // foo
...

/// result 这么写可以很清晰的知道实例中有这么一个属性,不过这个值最好是那些不需要new时动态传值的属性
class Computed {
    result = 0;
    constructor(x,y) {
        this.x = x;
        this.y = y;
    }
    add () {
        this.result = this.x + this.y;
    } 
}
let result = new Computed(1,2);
result.result; // 0
result.add();
result.result; // 3
  • 私有属性
    • 私有属性,只能在类的内部使用,如果在类的外部使用,就会报错。
    • # 用于方法前,可以形成私有方法
class Rectangle {
  #height = 0;
  #width;
  constructor(height, width) {    
    this.#height = height;
    this.#width = width;
  }
  #getValue () {
    console.log(`private ${this.#height} - ${this.#width}`);
  }
  getValue () {
    this.#getValue();
  }
}
let r1 = new Rectangle(10,10)
r1.getValue() // private 10 - 10
r1.#height		// Uncaught SyntaxError: Private field '#height' must be declared in an enclosing class
r1.#getValue() // Uncaught SyntaxError: Private field '#getValue' must be declared in an enclosing class
  • new.target 属性
    • 该属性一般用在构造函数之中,返回 new 命令作用于的构造函数,如果构造函数不是通过 new 命令调用的,new.target 会返回 undefined
function Person (name) {
	if (new.target !== undefined) {
		this.name = name;
	} else {
		throw new Error('必须使用new命令生成实例')
	}
}
new Person('foo'); // Person {name: "foo"}
Person('foo'); // Uncaught Error: 必须使用new命令生成实例
...

/// class内部调用new.target,返回当前class的name
class Foo {
	constructor () {
		console.log(new.target === Foo);
	}
}
new Foo();
// true
...

// 如果子类继承父类时,new.target会返回子类
class Foo {
	constructor () {
		console.log(new.target === Foo);
	}
}
class Bar extends Foo {
	constructor () {
		console.log('子类触发');
		super();
	}
}
new Foo();
// true
// 此时 new.target 为 Foo
...
new Bar();
// 子类触发
// false
// 此时 Foo 中的 new.target 为Bar

利用这个特点,可以写出不能独立使用,必须继承才能使用的类

class Foo {
	constructor () {
		if(new.target === Foo) {
			throw new Error('本类不能实例化');
		}
	}
}

class Bar extends Foo {
	constructor() {
		super();
	}
}

new Foo();
// 报错
new Bar();
// 正常使用

class 继承

  • class 可以通过 extends 关键字进行继承
    • 子类可以通过 super 关键字表示父类的构造函数,用来新建父类的this对象
    • 子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错(
      • ES5 的继承实质是创建子类的的实例对象 this,然后再将父类方法添加到 this
      • ES6 的继承机制是先将父类的实例对象的属性和方法加到 this 上,然后再用子类的构造函数修改 this。所以如果继承类不调用 super 方法,就得不到和父类同样的实例属性和方法,继承类后续的操作也就无法进行
class Foo {
	constructor () {
		console.log('Foo');
	}
}
class Baz extends Foo {
	constructor () {
		console.log('Baz');
	}
} 
new Baz();
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
...
class Baz extends Foo {
	constructor () {
		console.log('Baz');
		super();
	}
} 
// Baz
// Foo

如果子类的 constuctor 方法没有定义,默认情况下会添加super方法

class Foo {
	constructor () {
		console.log('Foo');
	}
}
class Baz extends Foo {} 
new Baz();
// Foo

如果子类要调用 this 关键字,需要在 super 方法之后才能调用

class Foo {
	constructor () {
		console.log('Foo');
	}
}
class Baz extends Foo {
	constructor (color) {
		this.color = color;
		super();
	}
} 
new Baz('red'); 
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
  • extends 可以跟多种类型的值
    • 除了 class,还是可以是任何有 prototype 属性的函数
    • 后面可以跟随原生构造函数
      • ES5 是无法做到继承原生构造函数的能力,是因为 ES5 是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数
      • ES6 可以,是因为 ES6 的继承机制是先新建父类的实例对象this,然后再用子类的构造函数的修饰符 this,使得父类的所有行为都可以继承
function Foo () {}
Foo.prototype.info = 'Foo';
class Baz extends Foo {
	print () {
		console.log(this.info);
	}
}
let baz = new Baz();
baz.print(); 
// Foo
...

/// 继承原生构造函数
class myArray extends Array {
	constructor (...args) {
		super(...args);
		this.history = 'v0.0.1'
	}
}
let arr = new myArray();
arr.push(1);
arr[0]; 
// 1
/// 此时的 myArray 行为和 Array 原生构造函数一致,区别在于里面包含了我们自定义的属性
  • super 关键字作为函数调用时,代表父类的构造函数,子类的构造函数必须执行一次super 函数
    • super 作为函数时,只能用在子类的构造函数中,用在其他地方会报错
      • super 内部的 this 指向的子类实例
    • super 作为对象时,在普通方法中,指向父类的原型对象,在静态方法中,指向父类
class Foo {}
class Baz extends Foo {
	m () {
		super();
	}
}
// Uncaught SyntaxError: 'super' keyword unexpected here
...

class Foo {
    constructor() {
        console.log(this)
    }
}
class Baz extends Foo {}
new Foo(); // Foo {}
new Baz(); // Baz {} super 内部 this 指向子类
...

class Foo {
	info () {
		console.log('Foo')
	}
}
class Baz extends Foo {
	show () {
		super.info();	// 调用父类的方法
		console.log('Baz');
	}
}
let baz = new Baz();
baz.show();
// Foo
// Baz

因为 super 作为对象使用时,指向的是父类的原型对象,所以定义在父类实例上的方法或属性,无法通过 super 调用

class Foo {
	constructor () {
		this.info = 'Foo'
	}
}
class Baz extends Foo {
	showInfo () {
		return super.info;
	}
}
let baz = new Baz();
baz.showInfo(); 
// undefined 找不到,因为 Foo.prototype 上没有 info 属性

Object.getOwnPropertyNames(Foo.prototype); 
// ["constructor"]
...

/// 如果我们给原型添加一个info属性那就不一样了
Foo.prototype.info = 'Foo.prototype';
Object.getOwnPropertyNames(Foo.prototype);
// ["constructor", "info"]

baz.showInfo();
// "Foo.prototype" 

子类普通方法中通过 super 调用父类的方法时,父类方法内部的 this 指向当前子类实例

class Foo {
	constructor () {
		this.info = 'Foo'
	}
	print () {
		console.log(this.info);
	}
}
class Baz extends Foo {
	constructor () {
		super();
		this.info = 'Baz';
	}
}
let baz = new Baz();
baz.print();
// Baz
/// 虽然这里我们调用的是父类方法,但其内部的 this 指向是是当前子类,所以输出的 info 为当前子类属性

子类中如果通过 super 对某个属性赋值,就是直接对子类属性赋值,注意读取是直接读取父类的原型

class Foo {
	constructor () {
		this.info = 'Foo';
	}
}
class Baz extends Foo {
	constructor () {
		super();
		super.info = 'Baz';
		console.log(this.info);
		console.log(super.info);
	}
}
new Baz();
// Baz
// undefined

在静态方法中,super 指向父类

class Foo {
	static myMethod (msg) {
		console.log('static', msg)
	}
	myMethod (msg) {
		console.log('instance', msg)
	}
}
class Baz extends Foo {
	static newMethod (msg) {
		super.myMethod(msg);
	}
	newMethod (msg) {
		super.myMethod(msg);
	}
}
Baz.newMethod(1);
// static 1 
/// 此时静态方法中的 super 指向的是父类,此处相当于调用Foo.myMethod(1)
...

let baz = new Baz();
baz.newMethod(2);
// instance 2 
/// 此时普通方法内 super 指向的是父类的原型对象,相当于是调用Foo.prototype.myMethod(2)

子类的静态方法中通过 super 调用父类方法时,方法内部的this 指向当前的子类,而不是子类实例

class Foo {
	constructor () {
		this.x = 1;
	}
	static print () {
		console.log(this.x);
	}
}
class Baz extends Foo {
	constructor () {
		super();
		this.x = 2;
	}
	static show () {
		super.print();
	}
}
Baz.x = 3;
Baz.show();
// 3
/// 如果不指定 x 为3,这里直接调用的话会报 undefined

使用 super 时,必须显示指定是作为函数还是对象使用,否则会报错

class Foo {}
class Baz extends Foo {
	constructor () {
		super();
		console.log(super)
	}
}
new Baz();
// Uncaught SyntaxError: 'super' keyword unexpected here
// console.log(super) 此处并不知道super是函数还是对象

类的 prototype 属性和 __proto__ 属性

class 存在两条继承链:

  • 子类的 __proto__ 属性,表示构造函数的继承,总指向父类
  • 子类的 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的prototype 属性
  • 子类实例的 __proto__ 属性的 __proto__ 属性,指向父类实例的__proto__ 属性
    • 子类的原型的原型,是父类的原型

之所以会存在两条继承链,是因为类的继承按照下面模拟实现:

  • 子类的实例继承父类的实例
  • 子类继承父类的静态属性
class Foo {}
class Baz extends Foo {}
Baz.__proto__ === Foo; // true 指向父类
Baz.prototype.__proto__ === Foo.prototype; // true 指向父类的 prototype
...

class Foo {}
class Baz extends Foo {}
let baz = new Baz();
let foo = new Foo();
baz.__proto__.__proto__ === foo.__proto__;
// true