JavaScript/基于对象编程
面向对象编程(OOP)是一种软件设计范式,它起源于 1960 年代,并在 1990 年代流行起来。它力求模块化、可重用性、封装和隐藏状态(数据)和行为(函数)、在泛化、继承等层次结构中进行设计。
它允许组件尽可能模块化。特别是,当创建一个新的对象类型时,期望它应该能够在不同的环境或新的编程项目中工作,而不会出现问题。这种方法的优点是开发时间更短,调试更容易,因为您在重新使用已被证明有效的程序代码。这种“黑盒”方法意味着数据进入对象,其他数据从对象中出来,但内部发生的事情,您无需关心。
随着时间的推移,人们开发了不同的技术来实现 OOP。最流行的是基于类和基于原型的方法。
类是定义了一组结构相同对象的各个方面(状态和行为)的蓝图。蓝图称为类,对象是该类的实例。C 家族语言 的流行成员,尤其是 Java、C++ 和 C#,用这种基于类的方法实现 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."
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 引擎将在“父”对象、“祖父母”对象等中搜索它。这称为原型链。
所有这些都以相同的方式适用于用户定义的对象和系统定义的对象,例如Arrays
或Date
。
自从 EcmaScript 2015 (ES6) 以来,语法提供了像class
或extends
这样的关键字,这些关键字用于基于类的方法。即使引入了这样的关键字,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”。
下一个脚本演示了原型链。它从用户定义的变量myArray
和theObject
开始。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.getPrototypeOf
、Object.setPrototypeOf
和Object.create
。
该脚本定义了两个类Adult 和Child,它们具有一些内部属性,其中一个属性是一个方法。关键字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
和方法showFamily
在Adult 类中定义。但它们在Child 类中也是已知的。
请再次注意,JavaScript 中这种基于类的继承是在基于原型的经典方法之上实现的。