类是用于创建对象的模板,类只是让对象原型的写法更加清晰、更像面向对象编程的语法。
看个例子
class Person {
// 构造函数
constructor(name, age) {
this.name = name
this.age = age
}
// 方法
say(){
console.log('我能说话')
}
}
// 实例化
let zs = new Person('张三', 24)
// 实例化
let ls = new Person('李四', 24)
console.log(zs)
console.log(ls)
是不是跟构造函数很像?下面我们会讲类与构造函数之间区别,我们先了解下类的基本用法。
类是“特殊的函数”,就像定义的函数表达式和函数声明一样,类语法有两个组成部分:类表达式和类声明。
// 类声明
class Point {
constructor() {
}
}
// 类表达式
let Point = {
constructor(){
}
}
函数声明和类声明之间的一个重要区别,函数声明会提升,类声明不会。需要先声明类,然后再访问它。
// 构造函数会变量提升
let son = new Person('zs', 24)
// Person {name: 'zs', age: 24}
// 类不会变量提升,导致引用异常
let classSon = new ClassPerson('classZs', 48)
// Uncaught ReferenceError: Cannot access 'ClassPerson' before initialization
// 构造函数
function Person(name, age) {
this.name = name
this.age = age
}
// 类
class ClassPerson {
constructor(name, age) {
this.name = name
this.age = age
}
}
一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。
class Point {
}
// 等同于
class Point {
constructor() {}
}
constructor()方法什么时候被执行呢?在实例化的时候会自动调用该方法。constructor()方法默认返回实例对象(this)
class Point {
constructor() {
// 通过new命令生成对象实例时,会执行constructor方法
console.log('我执行了')
// 返回的this是实例对象
console.log(this)
}
}
let p = new Point()
类的实例化一定要使用new,否则会报错。这也是跟构造函数的一个主要区别。
// 构造函数
function Point1() {
}
// 可以不使用new,当成普通函数执行
let p1 = Point1()
// 类
class Point {
constructor() {
console.log('我执行了')
console.log(this)
}
}
// 类不使用new会报错
// Uncaught TypeError: Class constructor Point cannot be invoked without 'new'
let p = Point()
类相当于实例的原型,所有在类中定义的方法(属性),都会被实例继承。如果在一个方法(属性)前,加上static关键字,就表示该方法(属性)不会被实例继承,而是直接通过类来调用。
class Person {
static personAge = 28
constructor(name, age) {
this.name = name
this.age = age
}
static getAge(age) {
return this.personAge + age
}
}
let zs = new Person('zs', 28)
// 静态属性只能通过类来访问
console.log(Person.personAge) // 28
// 静态属性实例不能使用
console.log(zs.personAge) // undefined
// 静态方法只能通过类来访问
Person.getAge(28)
// 静态方法实例不能使用
// zs.getAge();
// Uncaught TypeError: zs.getAge is not a function
// 执行会报错,因为this在严格模式下是underfined
// 这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到getAge方法而报错。
let getAge = Person.getAge
getAge(18)
// Uncaught TypeError: Cannot read properties of undefined (reading 'personAge')
尽管静态方法(属性)不能被实例使用,但是父类的静态方法,可以被子类继承(继承那边会介绍)。
私有方法(属性),是只能在类的内部访问的方法和属性,外部不能访问。这也是比较常见的需求,有利于代码的封装。 然而私有方法(属性)的定义之前一直不是很友好,在ES2022正式为class添加了私有属性,方法是在属性名之前使用#表示。
class Person {
// 私有属性
#name = '我能说话了'
// 私有方法
#say() {
// 引用私有属性
console.log(this.#name)
}
// 可能这样间接调用私有方法
indirectSay() {
this.#say()
}
}
let p = new Person()
// p.#name
// 报错 Uncaught SyntaxError: Private field '#name' must be declared in an enclosing class
// p.#say()
// 报错 Uncaught SyntaxError: Private field '#say' must be declared in an enclosing class
// 间接调用
p.indirectSay()
// 我能说话了
当然,如果在私有方法(属性)前面加上static关键字,表示这是一个静态的私有方法(属性)。
类可以通过extends关键字实现继承,让子类继承父类的属性和方法。
ES6 规定,子类必须在constructor()方法中调用super(),否则就会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()方法,子类就得不到自己的this对象。
为什么子类的构造函数,一定要调用super()?
原因就在于 ES6 的继承机制,与 ES5 完全不同。ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。
子类实例化,父类的构造函数必定会先运行一次
class Person {
constructor() {
console.log('person')
}
}
class Son extends Person {
constructor() {
super(); // 调用父类的构造函数
console.log('son')
}
}
let son = new Son()
// person
// son
另一个需要注意的地方是,在子类的构造函数中,只有调用super()之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有super()方法才能让子类实例继承父类。
class Person {
constructor() {
console.log('person')
}
}
class Son extends Person {
constructor() {
console.log(this)
super();
}
}
let son = new Son()
// 因为this在调用super之前调用了,导致报错
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
父类的静态属性和静态方法,是可以被子类继承。
class Person {
static count = 100
static obj = {name: 'zs obj'}
constructor() {
// 实例化时被调用
console.log('person')
}
say() {
console.log('我能说话了')
}
}
class Son extends Person {
constructor() {
super();
}
}
// 子类继承了父类的静态属性
Son.count--
Son.obj.name = 'son obj'
// 子类继承了父类的静态属性是浅拷贝,如果父类的静态属性的值是一个对象,那么子类的静态属性也会指向这个对象,因为浅拷贝只会拷贝对象的内存地址。
console.log('Son.count:%s', Son.count)
// Son.count:99
console.log('Son.obj:%o', Son.obj)
// Son.obj:{name: 'son obj'}
console.log('Person.count:%s', Person.count)
// Person.count:100
console.log('Person.obj:%o', Person.obj)
// Person5.obj:{name: 'son obj'}
// Object.getPrototypeOf()方法可以用来从子类上获取父类。
Object.getPrototypeOf(Son) === Person
子类无法继承父类的私有方法(属性),只能在定义它的类里面使用。
class Foo {
#p = 1;
#m() {
console.log('hello');
}
}
class Bar extends Foo {
constructor() {
super();
console.log(this.#p); // 报错
this.#m(); // 报错
}
}
但是子类可以通过父类的方法间接访问父类的私有方法(属性),说白了还是不能直接访问呗,只能通过定义它的类来使用。
class Foo {
#p = 1;
getP() {
return this.#p;
}
}
class Bar extends Foo {
constructor() {
super();
console.log(this.getP()); // 1
}
}
super这个关键字,既可以当作函数使用,也可以当作对象使用。
函数
代表父类的构造函数,子类的构造函数必须执行一次super()函数。 作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。
class Person {
constructor() {
console.log('person')
}
}
class Son extends Person {
constructor() {
// 必须执行一次
super();
}
say() {
super()
// Uncaught SyntaxError: 'super' keyword unexpected here (a
}
}
对象
super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class Person {
constructor() {
console.log('person')
}
say() {
console.log('我能说话')
}
}
class Son extends Person {
constructor() {
super();
}
say() {
// 在普通方法中,指向父类的原型对象
super.say()
}
}
//
let son = new Son()
son.say()
// 我能说话
由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。
class Person {
constructor(name) {
this.name = name
console.log('person')
}
say() {
console.log('我能说话')
}
}
class Son extends Person {
constructor(name) {
super(name);
}
say() {
console.log(super.name)
}
}
//
let son = new Son('zs')
// 由于name是父类的实例属性,不是原型属性,使用super获取不到
son.say()
// undefined
function Person(name, age) {
this.name = name
this.age = age
}
class ClassPerson {
constructor(name, age) {
this.name = name
this.age = age
}
}
let son = new Person('zs', 24)
let classSon = new ClassPerson('classZs', 48)
console.log(son)
console.log(classSon)
// 类的数据类型就是函数,类本身就指向构造函数
typeof ClassPerson // "function"
ClassPerson === ClassPerson.prototype.constructor // true
可以看到ClassPerson里面有一个constructor()方法,这就是构造方法,而this关键字则代表实例对象。这种新的 Class 写法,本质上与构造函数Person是一致的。
生成类的实例的写法,与构造函数完全一样,也是使用new命令。但是如果忘记加上new,像函数那样调用ClassPerson(),将会报错,构造函数可以直接调用。
构造函数添加方法到prototype属性,类的所有方法都定义在类的prototype属性,在类的实例上面调用方法,其实就是调用原型上的方法。
// 构造函数
function Person(name, age) {
this.name = name
this.age = age
}
// 在原型上增加方法
Person.prototype.say = function () {
console.log('构造函数在原型上添加方法')
}
// 定义类
class ClassPerson {
constructor(name, age) {
this.name = name
this.age = age
}
say() {
console.log('类在原型上添加方法')
}
}
let son = new Person('zs', 24)
// 查看实例及原型方法
console.log(son)
let classSon = new ClassPerson('classZs', 48)
// 查看实例及原型方法
console.log(classSon)
类定义say()方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加了会报错。
函数声明和类声明之间的一个重要区别在于,函数声明会提升,类声明不会。你首先需要声明你的类,然后再访问它。
// Person {name: 'zs', age: 24}
let son = new Person('zs', 24)
// Uncaught ReferenceError: Cannot access 'classPerson' before initialization
let classSon = new classPerson('classZs', 48)
function Person(name, age) {
this.name = name
this.age = age
}
class classPerson {
constructor(name, age) {
this.name = name
this.age = age
}
}
类继承
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
say() {
console.log('我会说话')
}
}
class Son extends Person {
constructor(name, age) {
super(name, age);
}
run() {
console.log('我能跑')
}
}
let zs = new Son('zs', 45)
console.log(zs)
构造函数继承
function Person1(name, age) {
this.name = name
this.age = age
}
Person1.prototype.say = function () {
console.log('我能说话')
}
class Son1 extends Person1 {
constructor(name, age) {
super(name, age);
}
run() {
console.log('我能跑')
}
}
let ls = new Son1('ls', 35)
console.log(ls)
类不能继承常规对象(不可构造的)。如果要继承常规对象,可以改用Object.setPrototypeOf()
// 普通对象
let Person2 = {
say() {
console.log('我能说话')
}
}
// 类不能直接继承普通对象
class Son2 {
constructor(name, age) {
this.name = name
this.age = age
}
run() {
console.log('我能跑')
}
}
// 类的方法都定义在prototype对象,所以类要继承普通对象,把普通对象设置原型到Son2.prototype即可
Object.setPrototypeOf(Son2.prototype, Person2)
let wu = new Son2('wu', 32)
console.log(wu.say())
// 原型 wu ----> Son2.prototype ----> Person2 ----> Object.prototype ----> null
类有私有(静态)属性的概念,构造函数没有。
class Person {
// 静态属性
static count = 100
// 私有属性
#name = 'zs'
// 私有方法
#say() {
console.log('我能说话了')
}
parentSay() {
// 调用私有方法
this.#say()
}
}