跳转到内容

JavaScript/基于对象编程

来自维基教科书,开放的书籍,开放的世界



面向对象编程(OOP)是一种软件设计范式,它起源于 1960 年代,并在 1990 年代流行起来。它力求模块化、可重用性、封装和隐藏状态(数据)和行为(函数)、在泛化、继承等层次结构中进行设计。

它允许组件尽可能模块化。特别是,当创建一个新的对象类型时,期望它应该能够在不同的环境或新的编程项目中工作,而不会出现问题。这种方法的优点是开发时间更短,调试更容易,因为您在重新使用已被证明有效的程序代码。这种“黑盒”方法意味着数据进入对象,其他数据从对象中出来,但内部发生的事情,您无需关心。

随着时间的推移,人们开发了不同的技术来实现 OOP。最流行的是基于类和基于原型的方法。

基于类的 OOP

[编辑 | 编辑源代码]

类是定义了一组结构相同对象的各个方面(状态和行为)的蓝图。蓝图称为,对象是该类的实例C 家族语言 的流行成员,尤其是 Java、C++ 和 C#,用这种基于类的方法实现 OOP。

基于原型 的 OOP

[编辑 | 编辑源代码]

基于原型的方法中,每个对象都存储其状态和行为。此外,它还有一个原型(如果它位于层次结构的顶层,则为null)。这样的原型是指向另一个更一般的对象的指针。所引用对象的所有属性在引用对象中也可用。在基于原型的方法中,类明确不存在。

// define a constructor for the class "Class4Person"
function Class4Person () {
  this.firstname =  "";
  this.lastname  =  "";
  this.age       =  0;
  return this;
};

// define a method "set_age()" for the class "Class4Person"
Class4Person.prototype.set_age = function (pYears) {
   // set age of person
   this.age = pYears;
}


// define a method "get_older()" for the class "Class4Person"
Class4Person.prototype.get_older = function (pYears) {
   // if pYear parameter is not set, define pYears=1
   pYears = pYears || 1;
   // increment age with pYears
   this.age += pYears;
}

// define a method "get_info()" for the class "Class4Person"
Class4Person.prototype.get_info = function () {
   // create an info string about person
   return "My name is " + this.firstname + " " + this.lastname + " and I am " + this.age + " old."
}

// create instance of Class4Person
let anna = new Class4Person();

// set attributes of instance "anna"
anna.firstname = "Anna";
anna.lastname = "Miller";

// call methods of instance "anna"
anna.set_age(15);  // anna.age = 15
anna.get_older(5); // anna.age = 20
anna.get_older();  // anna.age = 21

// create instance of Class4Person
let bert = new Class4Person();

// set attributes of instance "bert"
bert.firstname = "Bert";
bert.lastname  = "Smith";

// call method for instance "bert"
bert.set_age(30);  // age of instance "bert" is now set to 30 - bert.age = 30

// create info string for instances and show in console log
console.log(anna.get_info()); // "I am Anna Miller and I am 21 years old."
console.log(bert.get_info()); // "I am Bert Smith and I am 30 years old."

OOP in JavaScript "Two jackets for one body"

[编辑 | 编辑源代码]

JavaScript 的基石之一是根据基于原型的 OOP 的规则提供对象。对象由属性组成,这些属性是包含数据的键值对以及方法。其中一个属性始终是原型属性 '__proto__'。它指向“父”对象,并通过此实现了关系。


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

如果在任何对象上都缺少请求的属性,JavaScript 引擎将在“父”对象、“祖父母”对象等中搜索它。这称为原型链

所有这些都以相同的方式适用于用户定义的对象和系统定义的对象,例如ArraysDate

自从 EcmaScript 2015 (ES6) 以来,语法提供了像classextends 这样的关键字,这些关键字用于基于类的方法。即使引入了这样的关键字,JavaScript 的基本原理并没有改变:这些关键字以与以前相同的方式导致原型。它们是语法糖,并被编译成传统的原型技术。

总之,JavaScript 的语法提供了两种在源代码中表达面向对象特征(如继承)的方式:“经典”和“class”风格。尽管语法不同,但实现技术略有不同。

经典语法

[编辑 | 编辑源代码]

从一开始,JavaScript 就使用“原型”机制来定义对象的父子关系。如果源代码中没有明确说明,则会自动发生这种情况。经典语法很好地展现了它。

要明确定义两个对象的父子关系,您应该使用方法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' and 'showFamily()' are undefined here!
      alert(child.firstName + " " + child.familyName);

      // set the intended prototype chain
      Object.setPrototypeOf(child, adult);
      // or:  child.__proto__ = adult;

      alert(child.firstName + " " + child.familyName);
      alert(child.showFamily());
    }
    </script>
  </head>

  <body id="body">
    <button onclick="go()">Run the demo</button>
  </body>
</html>

“adult”对象包含“familyName”和一个函数“showFamily”。在第一步中,它们在“child”对象中是未知的。setPrototypeOf 运行后,它们将变为已知,因为“child”的原型不再指向默认的“Object”,而是指向“adult”。

下一个脚本演示了原型链。它从用户定义的变量myArraytheObject 开始。myArray 是一个包含三个元素的数组。第 6 行的赋值操作将theObject 设置为同一个数组。循环显示theObject 的原型,并将原型链的下一级分配给它。当到达层次结构的顶层时,循环结束。在本例中,原型是null

  function go() {
    "use strict";

    // define an array with three elements
    const myArray = [0, 1, 2];
    let theObject = myArray;

    do {
      // show the object's prototype
      console.log(Object.getPrototypeOf(theObject));  // Array[], Object{...}, null
      // or: console.log(theObject.__proto__);

      // switch to the next higher level
      theObject = Object.getPrototypeOf(theObject);
    } while (theObject);
  }

如您所知,属性是键值对。因此,可以直接使用“原型”属性的值来识别和操作原型。有趣的是,键的名称不是“prototype”,而是“__proto__”。这在第 11 行中显示。但是,我们建议您忽略这种技术,而是使用用于原型操作的 API 方法,例如Object.getPrototypeOfObject.setPrototypeOfObject.create

'class' 语法

[编辑 | 编辑源代码]

该脚本定义了两个类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 id="body">
  <button onclick="go()">Run the demo</button>
</body>
</html>

属性familyName 和方法showFamilyAdult 类中定义。但它们在Child 类中也是已知的。

请再次注意,JavaScript 中这种基于类的继承是在基于原型的经典方法之上实现的。

华夏公益教科书