面向对象编程(OOP)

面向对象编程(Object-oriented Programming,OOP)是一种软件设计范式,最早出现于20世纪60年代,并在20世纪90年代流行起来。它旨在实现模块化、可重用性、封装以及对状态(数据)和行为(函数)的隐藏,并通过概括、继承等层次结构进行设计。

OOP使组件尽可能模块化。特别是,当创建一个新的对象类型时,期望它能够在不同的环境或新的编程项目中正常工作。这种方法的好处是开发时间更短且调试更容易,因为你可以重用已经被验证的代码。这种“黑盒”方法意味着数据进入对象,另一组数据从对象中输出,但对象内部的操作则不需要特别关注。

随着时间的推移,已经开发了不同的技术来实现OOP。其中最受欢迎的是基于类的和基于原型的方法。


基于类的OOP

类是一个蓝图,用于定义一组结构上相同的对象的所有方面(状态和行为)。这个蓝图被称为类,而对象是该类的实例。C系列语言(如Java、C++和C#)通过基于类的方法实现OOP。


基于原型的OOP

在基于原型的方法中,每个对象都存储它自己的状态和行为。此外,每个对象都有一个原型(或在层次结构顶端时为 null)。该原型是指向另一个更通用对象的指针。引用对象的所有属性在引用它的对象中也可用。基于原型的方法中显式不存在类。

以下是一个基于原型的JavaScript示例:

// 定义类 "Class4Person" 的构造函数
function Class4Person () {
  this.firstname =  "";
  this.lastname  =  "";
  this.age       =  0;
  return this;
};

// 为 "Class4Person" 定义方法 "setAge()"
Class4Person.prototype.setAge = function (pYears) {
   this.age = pYears; // 设置人的年龄
}

// 定义方法 "getOlder()"
Class4Person.prototype.getOlder = function (pYears) {
   pYears = pYears || 1; // 如果参数未设置,默认为1
   this.age += pYears;   // 增加年龄
}

// 定义方法 "getInfo()"
Class4Person.prototype.getInfo = function () {
   return "My name is " + this.firstname + " " + this.lastname + 
          " and I am " + this.age + " years old.";
}

// 创建 "Class4Person" 的实例
let anna = new Class4Person();

// 设置实例 "anna" 的属性
anna.firstname = "Anna";
anna.lastname = "Miller";

// 调用 "anna" 的方法
anna.setAge(15);  // anna.age = 15
anna.getOlder(5); // anna.age = 20
anna.getOlder();  // anna.age = 21

// 创建另一个实例
let bert = new Class4Person();
bert.firstname = "Bert";
bert.lastname  = "Smith";
bert.setAge(30);  // bert.age = 30

// 打印信息
console.log(anna.getInfo()); // "My name is Anna Miller and I am 21 years old."
console.log(bert.getInfo()); // "My name is Bert Smith and I am 30 years old."

JavaScript中的OOP —— “一体两面”

JavaScript的核心之一是根据原型式OOP规则提供对象。对象由属性组成,这些属性是保存数据的键/值对以及方法。其中一个属性始终是 __proto__,它指向“父对象”,从而实现关系。

翻译

__proto__ 属性实现关系

let parent = [];
let child = {
  name:      "Joel",
  __proto__: parent,
};
console.log(Object.getPrototypeOf(child)); // Array []
┌─────────────────────┐          ┌─────────────────────┐
│        child        │    ┌──>  │       parent        │
├─────────────────────┤    |     ├─────────────────────┤
│  name: Joel         │    |     │  .... : ...         │
├─────────────────────┤    |     ├─────────────────────┤
│  __proto__: parent  │  ──┘     │  __proto__: ...     │  ──> ... ──> null
└─────────────────────┘          └─────────────────────┘

如果在某个对象上未找到请求的属性,JavaScript 引擎会在其“父对象”、祖父对象等中查找。这被称为 原型链

这种机制适用于用户定义的对象,也适用于像 ArrayDate 这样的系统定义对象。

自 ES6(EcmaScript 2015)以来,JavaScript 引入了 classextends 等关键字,用于支持基于类的方法。尽管添加了这些关键字,JavaScript 的底层机制未改变:这些关键字最终仍然基于原型机制实现,因此只是语法糖,最终会被编译为传统的原型技术。

总结而言,JavaScript 提供了两种表达面向对象特性的语法:经典语法和类语法。尽管语法不同,其底层实现只有些许差异。


经典语法

自 JavaScript 出现以来,就通过 prototype 机制定义对象的父/子关系。如果代码中未显式指定,这种关系会自动存在,经典语法很好地揭示了这一机制。

如果需要明确定义两个对象的父/子关系,可以使用 Object.setPrototypeOf 方法将一个对象的原型设置为另一个对象。完成后,父对象的所有属性(包括函数)都会在子对象中可用。

示例:设置原型链

<!DOCTYPE html>
<html>
<head>
  <script>
    function go() {
      "use strict";

      const adult = {
        familyName: "McAlister",
        showFamily: function() {return "The family name is: " + this.familyName;}
      };
      const child = {
        firstName: "Joel", 
        kindergarten: "House of Dwars"
      };

      // 在这里 'familyName' 和 'showFamily()' 是未定义的
      alert(child.firstName + " " + child.familyName);

      // 设置原型链
      Object.setPrototypeOf(child, adult);
      // 或者使用: child.__proto__ = adult;

      alert(child.firstName + " " + child.familyName);
      alert(child.showFamily());
    }
  </script>
</head>
<body>
  <button onclick="go()">Run the demo</button>
</body>
</html>

adult 对象包含属性 familyName 和方法 showFamily。在第一步中,这些属性和方法在 child 对象中不可用。但在运行 setPrototypeOf 方法后,child 的原型从默认的 Object 改为指向 adult,从而继承了这些属性。

示例:遍历原型链

下面的脚本展示了如何遍历原型链,从用户定义的数组对象 myArray 开始,逐层访问它的原型,直到到达原型链的顶端(null)。

function go() {
  "use strict";

  // 定义一个包含三个元素的数组
  const myArray = [0, 1, 2];
  let theObject = myArray;

  do {
    // 显示当前对象的原型
    console.log(Object.getPrototypeOf(theObject));  // Array[], Object{...}, null
    // 或者: console.log(theObject.__proto__);

    // 切换到更高层的原型
    theObject = Object.getPrototypeOf(theObject);
  } while (theObject);
}

属性是键/值对,因此可以直接使用 __proto__ 属性来标识和操作原型。然而,推荐使用 API 方法(如 Object.getPrototypeOfObject.setPrototypeOfObject.create)进行原型操作,而不是直接操作 __proto__


类语法

以下脚本定义了两个类:AdultChild。每个类都有其内部属性,其中一个是方法。关键字 extends 用于将两个类按层级关系连接起来。在第 21 行,通过关键字 new 创建了一个实例。

<!DOCTYPE html>
<html>
<head>
  <script>
  function go() {
    "use strict";

    class Adult {
      constructor(familyName) {
        this.familyName = familyName;
      }
      showFamily() {return "The family name is: " + this.familyName;}
    }
    class Child extends Adult {
      constructor(firstName, familyName, kindergarten) {
        super(familyName); // 调用父类构造函数
        this.firstName = firstName;
        this.kindergarten = kindergarten;
      }
    }

    const joel = new Child("Joel", "McAlister", "House of Dwars");
    alert(joel.firstName + " " + joel.familyName);
    alert(joel.showFamily());
  }
  </script>
</head>
<body>
  <button onclick="go()">Run the demo</button>
</body>
</html>

在这个示例中,Adult 类定义了 familyName 属性和 showFamily 方法。这些属性和方法通过继承在 Child 类中同样可用。

需要再次强调,JavaScript 中基于类的继承实际上是基于原型机制实现的,因此是对传统原型方法的语法扩展。

Last modified: Monday, 13 January 2025, 3:05 PM