JavaScript 继承——三种继承方法及其优劣

时间:2021-10-18 08:12:51

原文地址

 

本文内容

  • 目的
  • 继承的第一步——最简单的继承
  • 私有变量/成员和原型
  • 三种继承方式及其优劣
    • 基本的原型继承
    • Yahoo JavaScript 模块模式
    • 创建闭包的构造函数
    • 三种方法的代码执行结果
  • 封装

 

目的

这篇文章过后,你会觉得,在 JavaScript 中编写继承的代码不存在所谓的“圣杯”。我将介绍一些常用的方法,指出他们存在的问题(优劣),其中一个可能就是你所需要的。最重要的是,这是一课,可以用不同语法完成相同的事情,让你看见美妙而神奇的 JavaScript 世界。

 

继承的第一步——最简单的继承


先从一个继承的例子开始,看下面代码的优劣是什么。

从一个普通对象继承

这个例子很简单,只是从一个普通的对象继承。

代码段 1:

        // Basic object
        var Base = {
 
            // Public properties and methods
            dayName: "Tuesday",
 
            day: this.dayName,
 
            getDay: function () {
                return this.dayName;
            },
 
            setDay: function (newDayName) {
                this.dayName = newDayName;
            }
        };

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

目前为止的代码还可以。让我们创建一个对象 Sub,它继承 Base 对象。

代码段 2:

        // Using new Base() is not an option, 
        // since it isn't a constructor
        Sub.prototype = Base;
        function Sub() {
            // Constructor
        };

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

  • 测试

代码段 3:

        var a = new Sub();
        // Returns "Tuesday"
        alert(a.getDay());
 
        var b = new Sub();
        // Returns "Tuesday"
        alert(b.getDay());
 
        // Sets dayName to "Wednesday"
        a.setDay("Wednesday");
 
        // Returns "Wednesday"
        alert(a.getDay());
 
        // Returns "Tuesday"
        alert(b.getDay());
 
        // Returns undefined
        alert(b.day);

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

除了最后的那个返回 undefined 外,其他都跟预期的一样。问题就在于,当执行时,Base 对象尝试设置其属性,但没有任何内部的引用。对 day 属性使用一个方法可以解决这个问题。

代码段 4:

        day: function () {
            return this.dayName;
        }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

  • 好处

正如上面讨论的,你可以指定 Base 对象作为祖先来从原型(prototype)继承,但因为 Base 不是真正的构造函数,你没有用 new 关键字来调用它。基本上,这意味着,直到 Sub 对象的一个实例被创建,Base 构造函数才会执行。这是好事,因为在真正需要之前,你期望任何不必要的代码在构造函数中执行。就我个人而言,我不会在构造函数中放任何重要的东西,而是放一个init 方法,这样,当它被调用时,我就能够完全了。

  • 坏处

这里不好的地方是,所有的属性和方法都被声明为 Base 对象的内联,这就意味着,你不能利用原型的行为。另外,该方法没有途径放任何的私有变量。

 

私有变量/成员和原型


很多人都问我的一件事是,如果对象中有私有变量,那么,能否通过原型方法访问它们。答案是,很不幸——不能。然而,对于私有方法,还是能做到的,正如下面讲到的。

 

三种继承方式及其优劣


从这里开始,为了完成同一个结果,我将提出三种不同的语法。这三种语法都是非常不同的方式。另外,我也将展示私有变量和原型存在的问题,以及它们如何与私有方法一起工作。

基本的原型继承

首先是一般的原型继承,声明一个私有变量和私有方法。

代码段 5:

        // Basic Prototype inheritance
        function Base() {
 
            // Private variable
            var dayName = "Tuesday";
 
            // Private method
            function getPrivateDayName() {
                return dayName;
            }
 
            // Public properties and methods
            this.day = dayName;
 
            this.getDay = function () {
                return getPrivateDayName();
            };
 
            this.setDay = function (newDayName) {
                dayName = newDayName;
            };
        };
 
 
        Sub.prototype = new Base;
        function Sub() {
            // Constructor
        };

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

  • 好处

语法简单,从构造函数内可以访问所有东西。

  • 坏处

若想访问私有变量和私有方法,所有的东西都需要放在构造函数内,而不是通过原型。

这不是推荐的方法。

Yahoo JavaScript 模块模式

当涉及到单件对象(singleton object)时,我个人最喜欢 Yahoo JavaScript 模块模式(Yahoo JavaScript Module Pattern)。对于原型继承,你也可以对任何 Sub 对象使用它作为原型的祖先对象,如下所示:

代码段 6:

        // Yahoo JavaScript Module Pattern
        var Base = function () {
 
            // Private variable
            var dayName = "Tuesday";
 
            // Private method
            var getPrivateDayName = function () {
                return dayName;
            }
 
            // Public properties and methods
            return {
 
                day: dayName,
 
                getDay: function () {
                    return getPrivateDayName.call(this);
                },
 
                setDay: function (newDayName) {
                    dayName = newDayName;
                }
            };
        } ();
 
        // Using new Base() is not an option, 
        // since it isn't a constructor
        Sub.prototype = Base;
        function Sub() {
            // Constructor
        };

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

  • 好处

这个代码结构还不错,把私有和公共属性之间进行了很好地分离。变量 dayName 是私有变量,从外部无法访问,只能通过 getDaysetDay 函数访问。

  • 坏处

Sub.prototype = Base 这不是真正的构造函数,不能用 new 调用它。另外,return {…} 中所有的公共属性和方法都内联(inline)在对象中,因此,没有利用推荐的原型方法。

创建闭包的构造函数(Closure-Created Constructor)

下面代码,创建一个闭包,里边有构造函数,私有变量和方法,并把原型属性和方法指定给对象。然后,它返回实际的构造函数对象,因此,下一次运行时,它的行为就跟一个正常的构造函数一样,同时,闭包所有的属性和方法都仍然可访问。

到目前为止,这是最优雅的代码。

代码段 7:

        // Closure-created constructor
        var Base = (function () {
 
            // Constructor
            function Base() {
 
            }
 
            // Private variable
            var dayName = "Tuesday";
 
            // Private method
            function getPrivateDayName() {
                return dayName;
            }
 
            // Public properties and methods
            Base.prototype.day = dayName;
            Base.prototype.getDay = function () {
                return getPrivateDayName.call(this);
            };
 
            Base.prototype.setDay = function (newDayName) {
                dayName = newDayName;
            };
 
            return Base;
        })();
 
 
        Sub.prototype = new Base;
        function Sub() {
            // Constructor
        };

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

  • 好处

与代码段 6 相比,该代码使用了闭包,注意代码中的,公共属性和方法:day 变量、getDaysetDay 函数,有一个已初始化、具有对原型属性和方法进行完全控制的构造函数,反过来,又可以访问私有变量和方法。这个结构非常好,因为,在同一个代码块中,你有构造函数,属性和方法。

  • 坏处

唯一真正的缺点是,私有变量被限制到作用域,因此对于所有实例都是相同的。此外,构造函数外有私有变量,这点显得有点怪异。

三种方法的代码执行结果

对上面三种语法都使用下面代码测试。

代码段 8:

        var a = new Sub();
        // Returns "Tuesday"
        alert(a.getDay());
 
        var b = new Sub();
        // Returns "Tuesday"
        alert(b.getDay());
 
        // Sets dayName to "Wednesday"
        a.setDay("Wednesday");
 
        // Returns "Wednesday"
        alert(a.getDay());
 
        // Returns "Wednesday"
        alert(b.getDay());
 
        // Returns "Tuesday"
        alert(b.day);

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

为什么会有这样的结果?三种方法都创建一个私有变量,执行得也很好,结果也相同,但导致了,如果你改变某个实例对象的一个私有变量,那么,对所有的实例对象都改变了。也就是说,对上面三个方法,实例对象 a 若改变其私有变量 dayName,那么实例对象 b 的私有变量也会被改变。这样,它就更像一个私有的静态属性,而不是一个真正的私有属性。

So, if you want to have something private, more like a non-public constant, any of the above approaches is good, but not for actual private variables. Private variables only work really well with singleton objects in JavaScript.

所以,如果您想拥有某个私人的东西,它更像是一个非公有的常量(静态的,如 dayName 变量),而不是一个真正的私有变量,上面任何一个方法都可以。JavaScript 中,私有变量只在单件对象(singleton object)会很好的工作。

然而,对于私有方法,它的执行显得臃肿!你不公开它,而是在原型代码中使用它。

 

封装


正如你可以看到,JavaScript 提供很多方法来做相同的事情,不同的解决方案有不同的优点和缺点。我也认为,一个重要的经验是,在代码所能做的(提供预期结果)与对运行时、执行和重用来说最优之间,是不同的。选择对你来说,最合适的方式。

 

下载 Demo