使用纯 JavaScript 实现真正的继承






4.25/5 (6投票s)
本文介绍了一种使用 JavaScript 实现继承的技术,包括从基类继承接口、实现和数据。
纯 JavaScript 实现真正继承的示例
更新:本文最初写于 1999 年,侧重于现在称为“类式继承”的方法。本文已更新,还介绍了如何使用“差异继承”来实现相同的行为。
本文介绍了几种使用 JavaScript 实现继承的技术,包括从基类继承接口、实现和数据。在此示例中,有一个基类 Animal
,它有一个名为 lifespan
的数据成员。Animal
有一个名为 getSpeechText()
的虚函数,它应返回一个特定动物在被要求说话时会说出的消息。通用的 Animal
类只是说它不会说话。派生类应该重写并返回一些文本,例如狗的“汪汪”或猫的“喵”。Animal
类还有一个名为 talk()
的非虚函数,它只调用 getSpeechText()
来获取文本,然后将其显示到控制台。lifespan
字段初始化为默认值 100
。它是一个 public
数据成员,任何派生类都可以访问并为其提供初始值(以更改基类设置的默认值 100
)。最后,通过给每个 Animal 一个名字来演示构造函数参数。
getSpeechText()
方法说明了**接口继承**,派生类可以重写基类的行为。talk()
方法说明了**实现继承**,派生类可以直接从基类继承功能(无需重写)。lifespan
数据成员说明了**数据继承**,派生类继承基类引用的数据。
下面的示例将通过 C++ 和 JavaScript 来演示这些特性。
此示例创建了两个派生自 Animal
的类,分别称为 Dog
和 Human
。两者都重写了 getSpeechText()
方法。只有 Dog
类更改了 lifespan
数据成员(将其设为 15)。Human
类使用基类中初始化的默认值 100。
“类式继承”方法展示了一种更传统的面向对象编程形式,对于来自 C++、Java 或 C# 背景的程序员来说会比较熟悉。“差异继承”方法展示了一种更原型的对象继承技术,尽管没有使用“闭包”,因为它们对于说明继承不是必需的。
这两种技术都利用了用户定义类的 prototype
属性可以被更改以指向一个对象。因此,如果我们指向派生类的 prototype
属性指向基类的一个新实例,那么责任链将包含这个实例。这样,可以通过派生类实例来访问基类成员,前提是派生类没有定义同名的成员。然而,“差异继承”示例通过 ECMAScript 5 对 Object.create 的支持间接使用了原型。
代码使用了包含在内的 writeln()
实用函数。它假设脚本运行在一个非常基础的 HTML 页面上。
index.html
<html>
<body>
<div id='output'/>
<script src="Animal.js"></script>
</body>
</html>
还有许多命令行工具可用于运行 JavaScript 文件,例如 Node.js、Mozilla 的 js (jsshell)、jsdb 和 cscript.exe (JScript)。
但首先,让我们先从 C++ 版本开始,这样就可以清楚地知道我们期望类如何表现。
要使用已安装的 Visual C++ 编译 C++ 版本,请使用以下命令创建 Animal.exe
cl -EHsc Animal.cpp
C++ Animal.cpp
#include <iostream>
#include <string>
class Animal
{
public:
Animal(const std::string& name, int lifespan = 100)
: name( name ),
lifespan( lifespan )
{
}
void talk()
{
std::cout << getSpeechText() << std::endl;
}
virtual const char* getSpeechText()
{
return "Generic Animal doesn't talk";
}
const char* getName()
{
return name.c_str();
}
int lifespan;
std::string name;
};
class Dog : public Animal
{
public:
Dog(const std::string& name)
: Animal( name, 15 )
{
}
virtual const char* getSpeechText()
{
return "Bow wow";
}
};
class Human : public Animal
{
public:
Human(const std::string& name)
: Animal( name )
{
}
virtual const char* getSpeechText()
{
return "Hi there";
}
};
void main()
{
Dog fido("Fido");
Human bob("Bob");
std::cout << fido.getName() << "'s lifespan: ";
std::cout << fido.lifespan << " and talks by saying: " << std::endl;
fido.talk();
std::cout << bob.getName() << "'s lifespan: ";
std::cout << bob.lifespan << " and talks by saying: " << std::endl;
bob.talk();
}
使用类式继承的 JavaScript:Animal.js
function classicalInheritanceExample() {
// class Animal
function Animal(name) {
this.lifespan = 100;
this.name = name;
}
Animal.prototype.getSpeechText = function () { return "Generic Animal doesn't talk"; };
Animal.prototype.getName = function () { return this.name; };
Animal.prototype.talk = function () { log(this.getSpeechText()); };
// class Dog
function Dog(name) {
Animal.call(this, name);
this.lifespan = 15;
}
Dog.prototype = new Animal();
Dog.prototype.getSpeechText = function () { return "Bow wow"; };
// class Human
function Human(name) {
Animal.call(this, name);
}
Human.prototype = new Animal();
Human.prototype.getSpeechText = function () { return "Hi there"; };
// Example
var fido = new Dog("Fido"),
bob = new Human("Bob");
log(fido.getName() + "'s lifespan: " + fido.lifespan + " and talks by saying:");
fido.talk();
log(bob.getName() + "'s lifespan: " + bob.lifespan + " and talks by saying:");
bob.talk();
}
log("--------");
log("classicalInheritanceExample");
classicalInheritanceExample();
使用差异继承的 JavaScript:Animal.js
function differentialInheritanceExample() {
var createAnimal, createDog, createHuman;
// class Animal
createAnimal = (function () {
var animalMethods = {
getSpeechText: function () { return "Generic Animal doesn't talk"; },
talk: function () { log(this.getSpeechText()); }
};
return function (name) {
var animal = Object.create(animalMethods);
animal.lifespan = 100;
animal.name = name;
return animal;
};
}());
// class Dog
createDog = (function () {
var dogMethods = {
getSpeechText: function () { return "Bow wow"; }
};
return function (name) {
var dog = createAnimal(name);
dog.lifespan = 15;
dog.getSpeechText = dogMethods.getSpeechText;
return dog;
};
}());
// class Human
createHuman = (function () {
var humanMethods = {
getSpeechText: function () { return "Hi there"; }
};
return function (name) {
var human = createAnimal(name);
human.getSpeechText = humanMethods.getSpeechText;
return human;
};
}());
// Example
(function () {
var fido = createDog("Fido"),
bob = createHuman("Bob");
log(fido.name + "'s lifespan: " + fido.lifespan + " and talks by saying:");
fido.talk();
log(bob.name + "'s lifespan: " + bob.lifespan + " and talks by saying:");
bob.talk();
}());
}
log("--------");
log("differntialInheritanceExample");
differentialInheritanceExample();
日志记录
上面的代码假设有一个全局 log()
方法。你可以使用一个简单的类似以下的函数,它可以在大多数浏览器控制台窗口或 Node.js 控制台中进行日志记录。
var log = console.log.bind(console);
或者你可以使用这个通用的例程,它可以在大多数环境中工作。
/*global console, println, print, WScript*/
// General purpose logging routine that works for client and server side scripts.
//
// log(args) will print out args followed by a line-feed.
// 1. Works with all major browser:
// When working in a browser, output goes to both console.log and the browser window.
// Output will only go to the browser window if there's a
// <div> in the body section with id='output'. Example blank html:
// <html><body>
// <div id='output'/>
// <script src="script.js"></script>
// </body></html>
// 2. Works with common JavaScript command-line "shells":
// - Node.js
// - js (Mozilla's shell)
// - jsdb
// - JScript (use cscript.exe with wsh)
//
// Code below is optimized perform the environment detection only once/
//
var log = (function () {
var logRef = null,
output = null;
try {
logRef = console;
try {
output = document.getElementById('output');
} catch (ignore) {
}
if (output) {
logRef = function () {
var args = Array.prototype.slice.call(arguments).join(", ");
console.log(args);
output.innerHTML += args + "<br/>";
};
} else {
logRef = console.log.bind(console);
}
} catch (e) {
logRef = null;
}
if (!logRef) {
try {
logRef = println;
} catch (ignore) {
}
}
if (!logRef) {
try {
logRef = print;
} catch (ignore) {
}
}
if (!logRef) {
try {
logRef = WScript;
logRef = function () {
WScript.Echo(Array.prototype.slice.call(arguments).join(", "));
};
} catch (e) {
logRef = null;
}
}
if (!logRef) {
throw "no logger";
}
return logRef;
}());
输出
这些示例的输出应该类似于此。
Fido's lifespan: 15 and talks by saying:
Bow wow
Bob's lifespan: 100 and talks by saying:
Hi there
旧版 ECMAScript 支持
如果你使用的是 ECMAScript 5 之前的浏览器(即 Internet Explorer 9 之前的版本),你需要添加以下内容以确保“Object.create
”函数存在。// This is needed for pre-ECMAScript 5 environments (pre-IE 9)
if (typeof Object.create !== 'function') {
Object.create = function (o) {
function F() { return undefined; }
F.prototype = o;
return new F();
};
}
类式继承与差异继承的主要区别
类式继承的优点是语法对程序员来说似乎更自然:
var fido = new Dog("Fido");
fido.talk();
在这里,Dog
看起来像一个构造函数,就像在其他语言中一样。程序员调用 new
来创建一个实例,然后调用该实例上的方法。 一个缺点是,在这里并不清楚 Dog
() 是应该像类一样使用,还是像一个返回 Dog
实例的函数,在这种情况下不应该调用 new
。 在其他语言中,如果你尝试在类以外的任何东西上调用
,编译器会捕获它。 但是对于 JavaScript,表示“类”和函数的构造是相同的,所以如果你这样做,你不会得到任何类型的错误(运行时或编译时)。 最糟糕的是,如果 new
Dog
是一个构造函数,而程序员没有像他们应该的那样调用 new
,JavaScript 会将 Dog
() 作为函数调用,并将 全局 上下文传递给 this
指针。这意味着,当构造函数愉快地使用 this
指针来初始化其状态时,它实际上是在向 Web 浏览器窗口对象分配属性。 这种类型的错误很难及早发现。JavaScript 的设计者本应该在调用构造函数(即不带 new
)时将 this
设置为 null 或 undefined。 已经提出了几种解决方案来解决这个问题,例如命名约定,以明确哪些是构造函数,哪些不是,以及/或让每个构造函数验证其 this
指针不是全局指针。 使用 Google 的 Closure Tools 和 Microsoft 的 TypeScript 等其他工具可以防止发生此类问题。 然而,目前公认的最佳实践是完全避免使用构造函数来创建新对象。
差异继承,如本文所述,是解决此问题的另一种方法。 与其调用 new
Dog
,我们只需提供一个函数来为我们创建一个 Dog
对象:
var fido = createDog("Fido");
fido.talk();
createDog
基本上是一个工厂。 虽然在此未演示,但此技术还允许使用闭包来管理 private
的对象状态。
jsPerf 的简单性能实验表明,差异继承实际上更快。现在请在你的浏览器中运行该测试,这将使整体收集更好:http://jsperf.com/inheritanceperf/6。
历史
- 1999 年 2 月 9 日:首次发布(参考:链接)
- 2010 年 8 月 23 日:文章更新(更正了少量错误、拼写)
- 2014 年 2 月 21 日:提供了差异继承和类式继承的示例