进阶篇:Class(类)

在面向对象编程(OOP,Object-Oriented Programming)中,类定义了一类对象的特征。它是属性和方法的模板,用来画出具体实例的「蓝图」。

定义类

ES6 引入了更接近传统 OOP 的写法:用 class 关键字声明一个基于原型继承的类。

class 的大部分行为可以用 ES5 的构造函数加 prototype 模拟,所以常被称为语法糖。新写法让原型关系更清晰。私有字段、静态初始化块等是后来才进规范的,不能单靠 ES5 等价改写。

function Point(x, y) {
    this.x = x;
    this.y = y;
}
Point.prototype.toString = function () {};
const point = new Point(1, 2);
class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {}
}
const point = new Point(1, 2);
console.log(Point.prototype)
// {constructor: ƒ, toString: ƒ}
// constructor: class Point
// toString: ƒ toString()
// [[Prototype]]: Object

注意: 类的内部所有定义的方法,都是不可枚举的。

console.log(Object.keys(Point.prototype)) // []

实际上,类是 “特殊的函数”,就像函数表达式和函数声明一样,类语法有两个组成部分:类表达式和类声明。

类声明

声明一个类,可以使用带有 class 关键字的类名。

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

注意: 函数声明和类声明之间的一个重要区别在于,函数声明会提升,类声明不会。也就是说,类必须先声明,然后再访问,否则将抛出异常:Uncaught ReferenceError: Cannot access 'Point' before initialization

类表达式

类表达式是另一种定义类的方法。类表达式可以命名或不命名。命名类表达式的名称是该类体的局部名称。

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

需要注意: 类声明和类表达式的主体都执行在严格模式下。比如,构造函数、静态方法、原型方法、getter 和 setter 都在严格模式下执行。

constructor()

constructor() 是一个特殊的方法,称为构造函数,用于创建和初始化一个由 class 创建的对象。一个类只能拥有一个名为 "constructor" 的方法。如果类包含多个 "constructor" 方法,则将抛出 一个 SyntaxError:Uncaught SyntaxError: A class may only have one constructor

一个类必须有 constructor() 方法,如果没有显式定义,一个空的 constructor() 方法会被默认添加。

class Point {}

// 等同于
class Point {
    constructor() {}
}

constructor() 默认返回实例对象(即 this),也可以指定返回另外一个对象。原始值返回值会被忽略。

// 默认返回 this
const Point = class {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}
// 也可以返回其他对象
const Point = class {
    constructor() {
        return {
            x: 10,
            y: 10
        }
    }
}

一个构造函数可以使用 super 关键字来调用一个父类的构造函数。

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

注意: 类和普通构造函数的主要区别:类必须用 new 调用,否则报错;普通函数不用 new 也能执行。

静态方法

static 用来定义类的静态方法。调用时不需要实例化;实例上也没有这份方法(point.position 不是函数)。可以通过 ColorPoint.position()super.position()(在静态方法里)调用。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    static position(a, b) {
        console.log(this) // this 指向 Point 类
        return a - b
    }
}

console.log(Point.position(20, 100)) // -80
const point = new Point(10, 10)
console.log(point.position(20, 100)) // Uncaught TypeError: point.position is not a function

静态方法的 this 指向的是类,而不是实例。当调用静态或原型方法时没有指定 this 的值,那么方法内的 this 值将被置为 undefined。因为,class 体内部的代码总是在严格模式下执行。

const position = Point.position
console.log(position(20, 100)) // this 指向 undefined

注意: 静态方法也是可以从 super 对象上调用的。

class ColorPoint extends Point {
    static f1 () {
        super.position(20, 100) // 静态方法里的 super 指向父类
    }
}

实例属性

传统写法把实例字段写在方法里,赋给 this。如下方的 xy

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

也可以在类体外给构造函数或原型挂数据属性:

Point.color = "#00dddd";
Point.prototype.size = 10;

现行规范(ES2022)已支持公共和私有字段声明,可写在类体内,不必再放到外面。旧环境可用 Babel 转译。

公有实例字段(每个实例一份):

class Point {
    color = "#00dddd"
    constructor() {}
}

静态字段写在类上:

class Point {
    static origin = { x: 0, y: 0 }
}

私有字段声明:

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

注意: 私有属性的属性名必须包括 #,如果不带 #,会被当作另一个属性。

注意: 直接访问某个类不存在的私有属性会报错,但是访问不存在的公开属性不会报错。

getter / setter 函数

类内部可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class Point {
    constructor() {}
    get prop() {
        return 'getter';
    }
    set prop(value) {
        console.log('setter: '+ value);
    }
}

const point = new Point();
point.prop = 1; // 'setter: 1'
point.prop      // 'getter'

存值函数和取值函数是设置在属性的属性描述对象上。

Class 的继承

面向对象程序设计中最重要的一个概念是继承。

继承允许依据另一个类来定义一个类。也就是说,当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类(父类),新建的类称为派生类(子类)。

继承达到了重用代码功能和提高执行效率的效果,使得创建和维护一个应用程序变得更容易。

extends 关键字

class 可以通过 extends 关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。

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

super 关键字

super 可以当作函数使用,也可以当作对象使用。

当作函数使用时,用于在子类调用父类的构造函数。

ES6 规定,子类必须在 constructor() 方法中调用 super(),否则就会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用 super() 方法,子类就得不到自己的 this 对象。

如果子类没有定义 constructor(),会默认添加,并在里面调用 super(...args)。也就是说,派生类总有构造函数。

class ColorPoint extends Point {}

// 相当于
class ColorPoint extends Point {
    constructor(...args) {
        super(...args)
    }
}

注意: super 作为函数时,只能用在子类的构造函数之中,用在其他地方就会报错。

另外,super 也可以当作对象使用。当作对象使用时,需注意以下问题:

  • 子类的普通方法中,super 指向父类的原型对象
  • 子类的普通方法中,通过 super 对象调用父类的方法时,方法内部的 this 指向当前的子类实例
  • 子类的静态方法中,super 指向父类
  • 子类的静态方法中,通过 super 对象调用父类的方法时,方法内部的 this 指向当前的子类
class ColorPoint extends Point {
    normalFn () {
        return super.move === Point.prototype.move
    }
    static staticFn () {
        return super.name === Point.name
    }
}
console.log(new ColorPoint().normalFn()) // true
console.log(ColorPoint.staticFn()) // true

注意: 在子类使用 super 时,后面必须跟一个参数列表或成员访问。 也就是说,必须显式指定是作为函数(super())、还是作为对象使用(super.xxx),否则会报错。

class ColorPoint extends Point {
    move () {
        console.log(super)
    }
}
console.log(new ColorPoint().move())
// Uncaught SyntaxError: 'super' keyword unexpected here

私有属性和私有方法的继承

父类所有的属性和方法,都会被子类继承,除了私有的属性和方法。

const Point = class {
    #x = 10
    #move() { }
}
class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y)
        this.#x
        this.#move()
    }
}
// Uncaught SyntaxError: Private field '#x' must be declared in an enclosing class
// Uncaught SyntaxError: Private field '#move' must be declared in an enclosing class

静态属性和静态方法的继承

父类的静态属性和静态方法,会被子类继承。

const Point = class {
    static defaultX = 10
    static defaultPoint = {
        x: 10,
        y: 10
    }
    static move() {
        console.log('move')
    }
}
class ColorPoint extends Point { }
console.log(ColorPoint.move())

注意: 静态属性并不是拷到子类上,而是沿构造函数的原型链查找(ColorPoint.__proto__ === Point)。子类没有自己的 defaultPoint 时,读写到的是父类那一份。若值是对象,改对象的字段会影响到父类上的同一个对象。

ColorPoint.defaultPoint.x = 100
console.log(Point.defaultPoint.x)

class 相关的一些属性和方法

new.target 属性

new 是从构造函数生成实例对象的命令。ES6 为 new 命令引入了一个 new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数。如果构造函数不是通过 new 命令或 Reflect.construct() 调用的,new.target 会返回 undefined,因此这个属性可以用来确定构造函数是怎么调用的。

class Point {
    constructor() {
        console.log(new.target === Point)
    }
}
console.log(new Point()) // true

getPrototypeOf()

Object.getPrototypeOf() 方法用来从子类上获取父类,可用于判断一个类是否继承了另一个类。

Object.getPrototypeOf(ColorPoint) === Point

prototype、__proto__

大多数浏览器的 ES5 实现之中,每一个对象都有 __proto__ 属性,指向对应的构造函数的 prototype 属性。Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链。

(1)子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。

(2)子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的prototype属性。

相关问题

JavaScript 是面向对象还是面向过程编程语言?

面向过程编程是一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。

面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能,它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装)。

面向对象编程是把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。

对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。

面向对象编程相比面向过程编程,具有更加丰富的特性(封装、抽象、继承、多态)。利用这些特性编写出来的代码,更加易扩展、易复用、易维护。

JavaScript 是一门基于原型、函数先行的多范式语言,支持面向对象、命令式和函数式编程。 它不是「比面向过程更高级」这种等级关系,而是同一门语言里可以混用多种风格。

JavaScript 的对象设计跟目前主流基于类的面向对象差异非常大。而事实上,这样的对象系统设计虽然特别,但是 JavaScript 提供了完全运行时的对象系统,这使得它可以模仿多数面向对象编程范式,所以它也是正统的面向对象语言。

为什么子类的构造函数,一定要调用 super()?

原因就在于 ES6 的继承机制,与 ES5 完全不同。ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即 “实例在前,继承在后”。ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即 “继承在前,实例在后”。

这就是为什么 ES6 的继承必须先调用 super() 方法,因为这一步会生成一个继承父类的 this 对象,没有这一步就无法继承父类。同理,在子类的构造函数中,只有调用 super() 之后,才可以使用 this 关键字,否则会报错。

这意味着,新建子类实例时,父类的构造函数必定会先运行一次。

参考链接

MDN - Class

阮一峰 Class 的继承

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-08-31 22:19:40

results matching ""

    No results matching ""