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

面向对象编程(OOP,Object-Oriented Programming)是一种数据与操作方法高度耦合的编程方法。本文将讨论JavaScript中如何使用传统方法实现面向对象编程,下一篇文章将介绍如何使用class关键字创建类及相关应用。

创建类和对象

使用函数开发时称为过程式编程,函数定义了操作代码,操作的数据由参数带入;而面向对象编程则以“对象(Object)”为中心,如果有human对象,指定姓名时使用human.name="张三"的代码形式,移动时使用human.moveTo(x,y)的代码形式。

类(class),也就是数据类型,其中可以定义数据项,如字段(field)和属性(property);也可以定义操作方法(method)等要素。在早期的JavaScript中并没有使用class关键字,类使用函数定义;字段和属性使用变量创建;方法同样由函数定义。下面的代码演示了类的创建。

JavaScript
<script>
    function Human(name, age = 0) {
        // 属性
        this.name = name;
        this.age = age;

        // 方法
        this.moveTo = function (x, y) {
            alert(`${this.name}移动到(${x},${y})`);
        };
    }
    //
    let tom = new Human("Tom", 25);
    alert(tom.age);  // 25
    tom.moveTo(10, 99);  // Tom移动到(10,99)
    //
    alert(typeof Human);  // function
    alert(typeof tom);  // object
</script>

代码中的Human看起来与前面文章中创建的函数并无太大区别,而关键就在于this关键字,它表示当前对象,其中,this.name和this.age定义为Human类型的属性,this.moveTo定义为方法,包括x和y两个参数,用于指定坐标数据。Human()中的参数用于向对象内部传递初始数据,其中name没有默认值,age的默认值为0;调用Human()创建对象时称为使用Huamn类的构造函数。

用面向对象编程的术语来说,上述代码定义的Human就是Human类。某个类类型的变量称为对象,它指向一个类的实例;而类的实例则使用new关键字创建,如下面这行代码。

JavaScript
let tom = new Human("Tom", 25);

代码中定义了tom对象,它指向Human类的实例,这个实例由代码“new Human("Tom",25)”创建,其中,"Tom"指定为对象中name属性的值,25指定为对象中age属性的值。

接下来的代码,使用alert()函数显示了tom.name属性值,并调用了tom.moveTo()方法。

最后,使用typeof运算显示Human的类型是function,而tom的类型是object;如果需要判断某个对象是不是某个类的实例,可以使用instanceof运算符,如下面的代码。

JavaScript
<script>
    function Human(name, age = 0) {
        // 属性
        this.name = name;
        this.age = age;

        // 方法
        this.moveTo = function (x, y) {
            alert(`${this.name}移动到(${x},${y})`);
        };
    }
    //
    let tom = new Human("Tom", 25);
    alert(tom instanceof Human);  // true
    alert(tom instanceof Date); // false
</script>

代码中,判断tom对象是Human类的实例,第一个输出为true;而tom对象不是Date类的实例,第二个输出为false。

面向对象编程中,基本的概念包括:类(class)、属性(property)、方法(method)、对象(object)、实例(instance)。其中,类是类型,类中可以使用this关键字定义属性和方法。某个类类型的变量称为对象,它指向类的实例,而实例一般由new关键字和类的构造函数创建。

实例成员与类成员

前面的示例中,使用this关键字定义的name和age属性、moveTo()方法称为实例成员,需要使用对象访问。类中的另一种成员称为类成员,也称为静态(static)成员,直接由类名引用,如下面的代码。

JavaScript
<script>
    function Factory() {
        Factory.counter += 1;
    }
    // 类属性
    Factory.counter = 0;
    // 类方法
    Factory.showCounter = function () {
        alert(`已创建${Factory.counter}个实例`);
    };
    //
    let f1 = new Factory();
    Factory.showCounter();
    let f2 = new Factory();
    Factory.showCounter();
</script>

代码中,定义了两个Factory类的类成员,分别是类属性counter和类方法showCounter。Factory类中,每一次创建对象都会将counter属性加1,而showCounter()方法则显示创建了几个实例。

类中的实例成员由this关键字定义,由对象访问;而类成员(静态成员)则是在类的外部使用类名定义,并且通过类名访问。

判断对象是否包含属性和方法

判断对象中是否包含指定的属性可以使用对象的hasOwnProperty()方法,如下面的代码。

JavaScript
<script>
    function Human(name, age = 0) {
        // 属性
        this.name = name;
        this.age = age;

        // 方法
        this.moveTo = function (x, y) {
            alert(`${this.name}移动到(${x},${y})`);
        };
    }
    //
    let tom = new Human("Tom", 25);
    alert(tom.hasOwnProperty("name")); // true
    alert(tom.hasOwnProperty("org")); // false
</script>

JavaScript中的方法是通过function关键字定义的函数实现的,所以,判断方法是否存在时,可以判断其是否为function类型,如下面的代码。

JavaScript
<script>
    function Human(name, age = 0) {
        // 属性
        this.name = name;
        this.age = age;

        // 方法
        this.moveTo = function (x, y) {
            alert(`${this.name}移动到(${x},${y})`);
        };
    }
    //
    let tom = new Human("Tom", 25);
    alert(typeof tom.moveTo);  // function
    alert(typeof tom.sayHello);  // undefined
</script>

代码中,对象存在的方法会返回类型function,而不存在的方法则返回undefined,可以类型判断方法是否存在,并进一步操作。

原型(prototype)

JavaScript的类都会有一个prototype属性,使用它可以在类的外部对其成员进行扩展,如下面的代码。

JavaScript
<script>
    function Human(name, age = 0) {
        // 属性
        this.name = name;
        this.age = age;

        // 方法
        this.moveTo = function (x, y) {
            alert(`${this.name}移动到(${x},${y})`);
        };
    }
    //
    Human.prototype.sayName = function () {
        alert(`My name is ${this.name}.`)
    };
    let tom = new Human("Tom", 25);
    tom.sayName(); // My name is Tom.
</script>

本例,使用prototype属性扩展了Human类的sayName()方法。

通过prototype属性还可以扩展JavaScript内置类的操作,如下面的代码。

JavaScript
<script>
    Date.prototype.isLeap = function () {
        let y = this.getFullYear();
        return (y % 400 === 0) ||
            (y % 100 !== 0 && y % 4 === 0);
    };
    //
    let d1 = new Date();
    alert(d1.isLeap()); // false(系统当前时间为2026年)
    //
    let d2 = new Date(2020, 5);  // 2020年6月
    alert(d2.isLeap()); // true
</script>

代码中为Date类扩展了isLeap()方法,用于判断其中的年份是否为闰年。

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

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

JavaScript
<script>
    let C1 = function() {
        this.arr = [];
        //
        this.add = function (e) {
            this.arr.push(e);
            return this;
        };
        //
        this.toString = function () {
            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"。