JavaScript面向对象编程(class实现)

前一篇文章介绍了在JavaScript中使用函数类型实现面向对象编程的方法,而新的标准中可以使用class关键字创建类,本文将讨论相关的应用。

创建类和对象

下面的代码演示了如何使用class关键字创建类。

JavaScript
<script>
    class Human
    {
        constructor(name, age = 0)
        {
            this.name = name;
            this.age = age;
        }
        //
        moveTo(x, y) {
            alert(`${this.name}移动到(${x},${y})`)
        }
    }
    //
    let tom = new Human("Tom", 25);
    tom.moveTo(10, 99);  // Tom移动到(10,99)
    alert(tom.age);  // 25
</script>

代码中,class关键字后指定类名。在类的定义中,constructor()是特殊的要素,称为构造函数,其功能是完成对象的初始化工作,如参数带入了name和age属性数据;构造函数中,同样使用this关键字指定实例属性name和age。接下来,定义的是moveTo()方法,其参数是移动的坐标数据,方法显示了name属性和移动信息。

创建class类后,定义了tom对象,使用构造函数指定了name和age属性值,然后调用了moveTo()方法,并显示了age属性的值。

此外, class关键字创建的类也可以使用变量的形式,如下面的代码。

JavaScript
<script>
    let Human = class
    {
        constructor(name, age = 0)
        {
            this.name = name;
            this.age = age;
        }
        //
        moveTo(x, y) {
            alert(`${this.name}移动到(${x},${y})`)
        }
    }
    //
    let tom = new Human("Tom", 25);
    tom.moveTo(10, 99);  // Tom移动到(10,99)
    alert(tom.age);  // 25
</script>

代码执行效果与前例完全一样。

类成员(静态成员)

类成员,也称为静态成员,在类中可以使用static关键字定义;静态成员需要使用类名访问,如下面的代码。

JavaScript
<script>
    class Human
    {
        static counter = 0;
        static report() {
            alert(`已创建${Human.counter}个对象`)
        }
        //
        constructor(name, age = 0)
        {
            this.name = name;
            this.age = age;
            //
            Human.counter++;
        }
        //
        moveTo(x, y) {
            alert(`${this.name}移动到(${x},${y})`)
        }
    }
    //
    let tom = new Human("Tom", 25);
    Human.report();
    let john = new Human("John", 26);
    Human.report();
</script>

本例的Human类中,首先定义了静态属性counter,并指定默认值为0;然后定义了静态方法report(),其中会显示创建了多少个Human对象。在构造函数constructor()中,每次创建Human类的实例,counter属性值都会加1。创建Human类之后,分别创建了tom和john对象,每次创建对象后调用静态方法report()方法显示创建的对象数量。

私有成员与属性的读写

私有成员是指只能在类的内部访问的成员。定义私有变量(字段)时,需要在变量名前添加#符号;除了定义内部使用的数据,私有变量还可保存属性值,比如,在设置属性值时需要对数据进行检查,正确的数据才保存到属性中;下面的代码演示了相关应用。

JavaScript
<script>
    class Human
    {
        constructor(name, sex = 0)
        {
            this.name = name;
            this.sex = sex;
        }
        //
        #sex = "";
        get sex() {
            return this.#sex;
        }
        set sex(value) {
            value = parseInt(value);
            if (value >= 0 && value <= 2)
                this.#sex = value;
            else
                this.#sex = 0;
        }
    }
    //
    let someone = new Human("Noname");
    alert(someone.sex); // 0
    someone.sex = 2;
    alert(someone.sex);  // 2
    someone.sex = 1;
    alert(someone.sex); // 1
    someone.sex = 9;
    alert(someone.sex); // 0
</script>

本例主要显示了sex属性的操作,首先在Human类中定义了私有变量#sex,用于保存性别数据。接下来,使用“get <属性名>() { }”代码结构定义了属性的读取操作,代码中直接返回this.#sex变量的值;而属性的设置操作则使用“set <属性名>(<属性值>) { }”代码结构,代码中,只有设置的属性值在0和2之间才保存到#sex变量中,否则使用默认值0。最后,创建了someone对象,并进行一些sex属性的设置和读取操作。

对于不正确的数据也可以使用throw语句抛出错误,由调用代码捕捉错误并处理,如下面的代码修改了sex属性的设置操作代码。

JavaScript
<script>
    class Human
    {
        constructor(name, sex = 0) {
            this.name = name;
            this.sex = sex;
        }
        //
        #sex = "";
        get sex() {
            return this.#sex;
        }
        set sex(value) {
            value = parseInt(value);
            if (value >= 0 && value <= 2)
                this.#sex = value;
            else
                throw new Error("sex属性值设置错误");
        }
    }
    //
    try {
        let someone = new Human("Noname");
        someone.sex = 9;
    } catch (ex) {
        alert(ex);
    }
</script>

代码中,当sex设置的数据不是0到2之间时,使用“throw new Error("sex属性值设置错误");”语句抛出错误。接下来,在try语句结构中使用错误捕捉机制,并显示错误信息。

实际应用中,get代码块定义了属性的读取操作,如果没有定义,则无法在类的外部读取属性值;set代码块定义了属性的设置操作,如果没有定义,则无法在类的外部设置属性值。

定义私有方法时也可以在方法名前使用#符号,下面通过一个简单的示例来观察。

JavaScript
<script>
    class C1 {
        #m1() {
            alert("m1");
        }

        m2() {
            this.#m1();
            alert("m2");
        }
    }
    //
    let obj = new C1();
    obj.m2();
</script>

C1类中,m1()方法定义为私有成员,m2()方法中首先调用了m1()方法,然后显示"m2"。最后,定义obj对象并调用m2()方法,执行结果会显示"m1"和"m2"。

可迭代对象

介绍函数时讨论了函数如何返回可迭代数据,对于类,同样可以创建可迭代对象。对象默认的迭代数据可以使用*号和[Symbol.iterator]定义的方法返回,如下面的代码。

JavaScript
<script>
    class C1 {
        #arr = [1, 1, 2, 3, 5, 8];
        //
        *[Symbol.iterator]() {
            for (let i = 0; i < this.#arr.length; i++) {
                yield this.#arr[i];
            }
        }
    }
    //
    let obj = new C1();
    for (let n of obj) {
        alert(n);
    }
</script>

代码中,Symbol.iterator是一个JavaScript内置的符号(Symbol)类型,用于指定方法返回可迭代数据;方法中,使用for...of循环访问了私有数组arr的元素,并通过yield语句指定每次迭代返回一个元素数据。最后,定义了obj对象,并通过for...of语句对其进行迭代访问,执行结果会逐一显示C1类中arr数组的数据1、1、2、3、5、8。

继承

面向对象编程中,继承是重复使用已有代码的重要方式,指定一个类继承另一个类时可以使用extends关键字,如下面的代码。

JavaScript
<script>
    class C1 {
        constructor(name) {
            this.name = name;
        }
        //
        #m1() {
            alert("C1.m1");
        }

        m2() {
            this.#m1();
            alert("C1.m2");
        }
    }
    //
    class C2 extends C1 {
        constructor(name) {
            super(name);
        }
        //
        m1() {
            super.m2();
            alert("C2.m1")
        }
    }
    //
    let obj = new C2("测试对象");
    obj.m1();
    alert(obj.name);
    obj.m2();
    //
    alert(obj instanceof C2); // true
    alert(obj instanceof C1); // true
</script>

本例,首先定义了C1类,包括三个要素:构造函数、m1()和m2()方法,其中m1()方法定义为私有方法,构造函数中使用参数name指定了name属性的值。

接下来定义了C2类,它定义为C1类的子类,此时,C1称为C2的超类,也称为基类或父类。C2类中,首先定义构造函数,并包含参数name,然后通过super(name)调用C1类的构造函数,会将name参数值设置到name属性中。C2类的m1()方法中,首先通过super.m2()调用了C1类的m2()方法,然后显示"C2.m1"。

代码的最后创建了C2类型的对象obj,并指定name为"测试对象"。调用obj.m1()方法时会依次显示"C1.m1"、"C1.m2"、"C2.m1"。接下来通过alert()函数显示obj.name属性值,会显示"测试对象"。最后,调用obj.m2()方法,实际会调用C1类的m2方法,依次显示"C1.m1"、"C1.m2";使用instanceof运算符测试obj是C2类的实例,同时也是其超类C1的实例。

下面对类的继承做一些小结:

  • 子类通过extends关键字指定继承的超类(也称为基类或父类)。
  • 子类中可以通过super()调用超类的构造函数。
  • 子类中可以通过super关键字调用超类中的非私有成员,如变量(字段)、属性和方法。
  • 子类可继承超类的非私有成员,即子类的对象可以调用父类的非私有变量(字段)、属性和方法。
  • 子类的对象使用instanceof运算符判断是否为超类的实例时也会返回true。

此外,使用原型(prototype)也是扩展类的常用方式。

链式调用方法和toString()方法

Set对象的add()和Map对象的set()方法都定义为链接方法,可以连续调用,这种方法实现的关键就是在不需要返回数据的方法中总是返回当前对象。toString()定义对象默认的文本信息。下面的代码演示了相关应用。

JavaScript
<script>
    class C1 {
        #arr = [];
        //
        add(e) {
            this.#arr.push(e);
            return this;
        }
        //
        toString() {
            return this.#arr.join(" | ")
        }
    }
    //
    let obj = new C1();
    obj.add(1).add(2).add(3);
    alert(obj);  // 1 | 2 | 3
</script>

代码中,C1类的add()对象定义为链式方法,在obj对象中连续添加了三个元素。toString()方法会返回所有元素由" | "连接的字符串,最后调用alert()函数直接显示obj对象的信息,显示结果为" 1 | 2 | 3"。