65.9K
CodeProject 正在变化。 阅读更多。
Home

使用纯 JavaScript 实现真正的继承

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.25/5 (6投票s)

2008年2月9日

CPOL

5分钟阅读

viewsIcon

36731

downloadIcon

56

本文介绍了一种使用 JavaScript 实现继承的技术,包括从基类继承接口、实现和数据。

纯 JavaScript 实现真正继承的示例

更新:本文最初写于 1999 年,侧重于现在称为“类式继承”的方法。本文已更新,还介绍了如何使用“差异继承”来实现相同的行为。

本文介绍了几种使用 JavaScript 实现继承的技术,包括从基类继承接口、实现和数据。在此示例中,有一个基类 Animal,它有一个名为 lifespan 的数据成员。Animal 有一个名为 getSpeechText() 的虚函数,它应返回一个特定动物在被要求说话时会说出的消息。通用的 Animal 类只是说它不会说话。派生类应该重写并返回一些文本,例如狗的“汪汪”或猫的“喵”。Animal 类还有一个名为 talk() 的非虚函数,它只调用 getSpeechText() 来获取文本,然后将其显示到控制台。lifespan 字段初始化为默认值 100。它是一个 public 数据成员,任何派生类都可以访问并为其提供初始值(以更改基类设置的默认值 100)。最后,通过给每个 Animal 一个名字来演示构造函数参数。

  1. getSpeechText() 方法说明了**接口继承**,派生类可以重写基类的行为。
  2. talk() 方法说明了**实现继承**,派生类可以直接从基类继承功能(无需重写)。
  3. lifespan 数据成员说明了**数据继承**,派生类继承基类引用的数据。

下面的示例将通过 C++ 和 JavaScript 来演示这些特性。

此示例创建了两个派生自 Animal 的类,分别称为 DogHuman。两者都重写了 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。 在其他语言中,如果你尝试在类以外的任何东西上调用 new,编译器会捕获它。 但是对于 JavaScript,表示“类”和函数的构造是相同的,所以如果你这样做,你不会得到任何类型的错误(运行时或编译时)。 最糟糕的是,如果 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 日:提供了差异继承和类式继承的示例
© . All rights reserved.