JavaScript编程
面向对象编程(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 引擎会在其“父对象”、祖父对象等中查找。这被称为 原型链。
这种机制适用于用户定义的对象,也适用于像 Array
或 Date
这样的系统定义对象。
自 ES6(EcmaScript 2015)以来,JavaScript 引入了 class
和 extends
等关键字,用于支持基于类的方法。尽管添加了这些关键字,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.getPrototypeOf
、Object.setPrototypeOf
和 Object.create
)进行原型操作,而不是直接操作 __proto__
。
类语法
以下脚本定义了两个类: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>
<button onclick="go()">Run the demo</button>
</body>
</html>
在这个示例中,Adult
类定义了 familyName
属性和 showFamily
方法。这些属性和方法通过继承在 Child
类中同样可用。
需要再次强调,JavaScript 中基于类的继承实际上是基于原型机制实现的,因此是对传统原型方法的语法扩展。