基本概念
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
class
的Generator
- 如果某个方法之前加上星号(
*
),就表示该方法是一个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
- 该属性一般用在构造函数之中,返回 new 命令作用于的构造函数,如果构造函数不是通过 new 命令调用的,
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