Skip to content

Class 语法

只能调用不能New,是一种函数。

JavaScript 中,定义构造函数与普通函数并无差别,这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。

Class 定义类(定义构造函数)

语法:

js
// 定义类
class Product {
    // 给实例设置属性 属性会直接添加到实例本身
    name = '汽车';
    price = 121.23;

    // 给实例设置方法 简写方式 这些方法会添加到实例的原型上
    buy(n) {
        console.log(`购买了${n}件${this.name}`);
    };

    addShopcar() {
        console.log('将' + this.name + '加入购物车!');
    };
}


console.log(Product);
console.log(typeof Product);    // function
console.log('');

// Product();                   // 不能调用 只能实例化

// 实例化
const p1 = new Product();       
console.log(p1);                // 返回Product类型的对象
p1.buy(45);                     // 购买了45件汽车
p1.addShopcar();                // 将汽车加入购物车!

特点:

js
1. 使用 class 关键字定义的类本质上仍然是构造函数使用 typeof 判断返回 function;
2. 使用 class 关键字定义的类构造函数不能被调用只能被实例化;
3.  class 里面使用简写方式为设置的方法会自动添加到实例的原型上;
4.  class 里面只能定义属性和方法如果有其他代码可以在方法内部写;
5. 以分号结尾

上面方式定义的类,具有如下特点:

  • 1)ES6 的类,本质上还是构造函数,使用 typeof 查看类的类型,返回function
  • 2)类可以被实例化,但是不能被调用,这是与之前的构造函数的主要区别。
  • 3)上述代码中定义的属性,会添加到类的实例上。
  • 4)上述代码中定义的方法,会添加到类的实例的原型上。
  • 5)constructor 被称之为构造方法,会在实例化的时候自动调用,可用于对属性初始化赋值。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
  • 6)类中的方法内使用 this,它默认指向类的实例,任然遵循谁调用指向谁的规则。
  • 7)类中可以定义属性和方法,但不能写其他语句,否则会报错;当然,方法的里面就可以写任何的语句了。

类中定义构造器方法

使用构造器方法给实例的属性进行赋值。构造器方法在实例化的时候会自动执行。

js
constructor(){}

class 语法可以直接添加访问器属性,不用像 ES5 那样使用 Object.defineProperty() 方法。

语法:

js
class Product {
    // 给实例设置属性
    name;
    price;

    // 构造器方法
    constructor(name, price) {
        this.name = name;
        this.price = Math.max(price, 100);
        this.num = Math.random();
    };
}

特点:

js
1. 构造器方法在实例化的时候会自动执行
2. 构造函数方法通常用于给属性进行赋初始值,构造器方法中的this指向实例
js
// 定义类
class Product {
    // 给实例设置属性
    name;
    price;

    // 构造器方法
    constructor(name, price) {
        this.name = name;
        this.price = Math.max(price, 100);
        this.num = Math.random();
    };

    // 给实例设置方法
    buy(n) {
        console.log(`购买了${n}件${this.name}`);
    };

    addShopcar() {
        console.log('将' + this.name + '加入购物车!');
    };
}


// 实例化   构造器方法在实例化的时候会自动执行
//构造函数方法 给属性赋初始值。实例中的 name='华为p40',price='199'
const p1 = new Product('华为p40', 199);     

//构造函数方法 给属性赋初始值。实例中的 name='iphone15',price='100'
//构造器方法在实例化的时候会自动执行,price=100
const p2 = new Product('iphone15', 99);   

console.log(p1);    //Object对象实例
console.log(p2);    //Object对象实例

//修改 Object对象实例 属性值
p1.name = '华为Nova11';     
console.log(p1);  	//Object对象实例
p1.buy(99);        //购买了99件华为Nova11

私有属性

类中的属性名前加 '#' 代表私有属性

  • 私有属性只能在类中的方法中通过this使用,在类的外部无法通过对象名使用。
  • 私有属性需要在赋值前提前声明

子类的代码不能更改父类定义的属性(这一点有别于其他编程语言)

语法:

js
// 定义类
class Product {
    // 给实例设置属性
    #name;
    #price;
    #num;

    // 构造器方法
    constructor(name, price) {
        this.#name = name;
        this.#price = Math.max(price, 100);
        this.#num = Math.random();
    };

    // 给实例设置方法
    buy(n) {
        console.log(`购买了${n}件${this.#name}`);
    };

    addShopcar() {
        console.log('将' + this.#name + '加入购物车!');
    };
}


// 实例化
const p1 = new Product('华为p40', 199);     
console.log(p1);            //Product对象

console.log(p1.name);       //undefined
console.log(p1.price);      //undefined
console.log(p1.num);        //undefined

p1.buy(12);                 //购买了12件华为p40

特点:

js
1. 私有属性只能在类中的方法中通过this使用,在类的外部无法通过对象名使用
2. 私有属性需要在赋值前提前声明

类方法的几种形式

js
class Person{
    // say定义在Person的原型对象(Person.prototype)上,类本身无法用,实例可以用,
    // this指向:谁调用指向谁
    say(){
        console.log("say is running,name: ",this)
    }
    // sleep定义在实例化对象(Person)上,类本身无法用,实例可以用,
    // this指向:谁调用指向谁
    sleep = function(){
        console.log("sleep is running,age: ", this)
    }
    // 箭头函数定义在 实例化对象上,类本身无法用,实例可以用
    // this指向:箭头函数没有this,this的指向取决于上层作用域中的this指向
    // 上层作用域中的this是CLass类,this的指向是实例,所以是Person实例
    rap = () => {
        console.log("rap is running,rap_this ",this)
    }
    // 静态方法,定义在Person的原型对象中(Person.prototype)的constructor属性中
    // 类本身的方法,类本身可以用,实例无法用,
    // this指向:指向类本身
    static eat(){
        console.log("eat is running,eat_this ",this)
    }
}

const p1 = new Person()
const p2 = new Person()

// 验证引用地址是否相同
console.log(p1.say === p2.say) //true 说明是在原型对象(Person.prototype)上
console.log(p1.sleep === p2.sleep) //false 说明是在实例化对象(Person)上,每一个实例对象都有一个自己的sleep方法
// 查看实例
console.log("info->p1",p1)  // 也能看到say方法和sleep方法的位置不同

// 只能类本身调用eat方法
Person.eat()

使用案例:

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Class功能验证</title>
</head>
<body>
    <div id="root">Class功能验证</div>
</body>
<script>
    class Person{
        constructor(name,age){
            this.name = name;
            this.age = age
        }
        state={
            name:"Hanser",
            age:18
        }
        // 函数形式中 this 谁调用,指向谁
        // say在Person的原型对象上,类本身无法用,实例可以用
        say(){
            console.log("say is running,name: ",this.state.name)
        }
        // sleep在实例化对象上p,类本身无法用,实例可以用
        sleep = function(){
            console.log("sleep is running,age: ", this.state.name)
        }
        // 使用static 设置eat为静态的方法,eat只会在Persion本身类上,实例上用不了
        static eat(){
            console.log("eat is running,eat_this ",this)
        }
        sing(){
            console.log("sing is running, sing_this ",this)
        }
        dance = function(){
            console.log("dance is running,dance_this ",this)
        }
        // 箭头函数中的this,箭头函数没有this,this的指向取决于上层作用域中的this指向
        // 上层作用域中的this是CLass类,this的指向是实例,所以是Person实例
        rap = () => {
            console.log("rap is running,rap_this ",this)
        }
    }
    // 实例化对象
    const p1 = new Person()
    const p2 = new Person()
    // 调用方法
    p1.say()
    p2.say()
    p1.sleep()
    p2.sleep()

    // 验证引用地址是否相同
    console.log(p1.say === p2.say) //true 说明是在原型对象(Person.prototype)上
    console.log(p1.sleep === p2.sleep) //false 说明是在实例化对象(Person)上,每一个实例对象都有一个自己的sleep方法
    // 查看实例
    console.log("info->p1",p1)  // 也能看到say方法和sleep方法的位置不同

    // static 语法调用
    // 只能类本身调用eat方法
    Person.eat() //  this 指向Person类本身,谁调用,this指向谁
    // 实例上调用static 静态方法
    // p1.eat() // 报错

    // this指向的问题
    // 定义一个对象,把say的引用地址和eat的引用地址赋值给f1和f2属性
    const obj = {
        name: "Yousa",
        f1: p1.sing,
        f2: p1.dance,
        f3:p1.rap
    }
    p1.sing() // this 指向Person实例,谁调用,指向谁
    p1.dance() // this 指向Person实例,谁调用,指向谁
    p1.rap() // this 指向Person实例,谁调用,指向谁

    obj.f1() // this 指向obj对象,谁调用,指向谁
    obj.f2() // this 指向obj对象,谁调用,指向谁
    obj.f3() // this 指向Person实例,原因因为箭头函数没有this,this的指向取决于上层作用域中的this指向,
    // 上层作用域中的this是CLass类,this的指向是实例,所以是Person实例
</script>
</html>

静态方法

构造函数本身的方法就是静态方法。

方法前面加static 关键字,就表示该方法不会添加到实例的原型上,而是添加到类本身上,被称为静态方法

如果静态方法包含 this 关键字,这个 this 默认指向的是类本身,任然遵循谁调用指向谁的规则。

  • 静态方法就是构造函数(类)自身的方法
  • 静态属性也是构造函数(类)自身的属性

语法:

js
  // 定义类
class Product {
    // 静态属性 静态属性语法还处提案阶段,没有正式纳入标准,6月更新。
    static name = '小乐';

	// 静态属性
    static getInfo() {
        console.log('我是 Product 类中的静态方法');
    }
}
js
// 定义类
class Product {
    // 给实例设置属性
    static name = '小乐';

    // 静态属性
    static getInfo() {
        console.log('我是 Product 类中的静态方法');
    }
}

// 添加Product静态方法
Product.getPrice = function() {
    console.log("我是Product添加的静态方法");
}
// 创建实例
p1 = new Product(name);
console.log(p1.name);               // undefined
console.log(Product.getInfo);       // ƒ getInfo() {...}
console.log(Product.name);          // "小乐"
Product.getInfo();                  // "我是 Product 类中的静态方法"
console.log(Product.getPrice)       // ƒ () {...}

特点:

js
静态方法就是构造函数(类)自身的方法

继承

class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

① extends 关键字实现继承

语法:

js
class 父类 {
    
}

class 子类 extends 父类 {
    
}

特点:

js
1. 一个父类可以被多个子类继承,一个子类只能继承一个父类;
2. 子类的实例的原型会自动设置成父类的一个实例; extends
   子类的实例的原型的 constructor 属性会自动指向子类; extends 
   与构造函数和构造函数之间继承(子类 父类)相同。

② 方法和属性的重写

  • 子类中定义的属性和方法如果与父类中定义的属性或方法重名,子类中会重写继承下来的属性和方法。
  • 如果子类中重写父类中的构造器方法,子类中构造器方法中必须先通过 super 关键字来调用父类的构造器方法,再进行其他操作。

语法:

js
class 父类 {
    name;
    price;
    say() {}
    eat() {}
}

class 子类 extends 父类 {
    name;
    say() {}
}

class 子类 extends 父类 {
  	constructor() {
        super();   // 将父类的构造器方法执行一次,父类中的参数需要传进来。
    }
}

特点:

js
1. 子类中定义的属性和方法如果与父类中定义的属性或方法重名,子类中会重写继承下来的属性和方法。
2. 如果子类中重写父类中的构造器方法,子类中构造器方法中必须先通过 super 关键字来调用父类的构造器方法,再进行其他操作。
js
// 定义类
class Product {
    // 给实例设置属性
    name;
    #num;
    price;

    // 构造器方法
    constructor(name, price) {
        this.name = name;
        this.price = Math.max(price, 100);
        this.#num = Math.random();
    };

    // 给实例设置方法
    buy(n) {
        console.log(`购买了${n}件${this.name}, 单价是${this.price}`);
    };

    addShopcar() {
        console.log('将' + this.name + '加入购物车!');
    };
}


// 定义子类
class Phone extends Product {
    color = 'red';

    constructor(name, price, color) {
        // 将父类的构造器方法调用一次
        super(name, price);
        // 设置自己的属性
        this.color = color;
    }
    discount(per) {
        this.price *= per;
    }
    buy() {
        console.log(`购买了${this.name},价格:${this.price},颜色:${this.color}`);
    }
}


// 实例化子类
const p = new Phone('华为P50', 89.9, 'green');
console.log(p); //Phone对象
p.buy(12); //购买了华为P50,价格:100,颜色:green

③ super 关键字

super 关键字可以作为对象使用,也可以作为函数使用。

super 关键字作为对象使用,具有如下特点:

js
1. super 关键字 写在使用 {} 声明对象时,只能用在简写形式的方法中,其他形式的方法一律报错;
2. super 表示方法所属的对象的原型;

super 的指向与谁调用该方法无关,只与定义方法时所在的对象的有关;

super 关键字作为函数使用,具有如下特点:

js
1. 在子类的构造器方法中使用 super, 只能在构造器方法中使用,其他方法中不能使用。
2. 此时 super 表示父类的构造器方法constructor:constructor(){}, 子类若重写父类构造器方法,必须先调用 super,然后再写自己的方法;
3. 在子类的构造器方法中,要求 super 必须写在最前面;

④ 继承内置类(内置构造函数)

Number String Boolean Math Date Array Function 这些都是内置构造函数。

使用自定义的类来继承这些内置构造函数,就是继承内置类。

js
 // 定义类 继承Array
class MyArray extends Array {
    #name;
    // 重写构造器方法
    constructor(name, ...args) {
        super(...args);
        this.#name = name;
    }
}

const ma = new MyArray('xiaole', 10,20,30,40,50,60);
console.log(ma);