在对象类声明中设置JavaScript原型函数

23

通常,我看到原型函数在类定义之外声明,就像这样:

function Container(param) {
    this.member = param;
}
Container.prototype.stamp = function (string) {
    return this.member + string;
}

var container1 = new Container('A');
alert(container1.member);
alert(container1.stamp('X'));

这段代码输出两个弹窗,值分别为"A"和"AX"。

我想在类定义内部定义原型函数。这样做有什么问题吗?

function Container(param) {
    this.member = param;
    if (!Container.prototype.stamp) {
        Container.prototype.stamp = function() {
            return this.member + string;
        }
    }
}

我试图这样做,以便能够访问类中的一个私有变量。但是我发现如果我的原型函数引用了一个私有变量,那么私有变量的值始终是在创建原型函数时使用的值,而不是对象实例中的值:

Container = function(param) {
    this.member = param;
    var privateVar = param;
    if (!Container.prototype.stamp) {
        Container.prototype.stamp = function(string) {
            return privateVar + this.member + string;
        }
    }
}
var container1 = new Container('A');
var container2 = new Container('B');
alert(container1.stamp('X'));
alert(container2.stamp('X'));

这段代码会产生两个警告,值分别为"AAX"和"ABX"。我希望输出为"AAX"和"BBX"。我想知道为什么不行,同时是否存在其他模式可以使用。

编辑:请注意,我完全理解对于这个简单的例子,最好的方法是使用闭包,如this.stamp = function() {},而根本不需要使用原型。这也是我采用的方法。但我正在尝试使用原型来了解更多相关知识,并想知道以下几点:

  • 在什么情况下使用原型函数比闭包更有意义?我只需要使用它们来扩展现有对象,例如 Date。我已经阅读到了闭包更快的文章。
  • 如果需要使用原型函数,将其定义在类内部,例如我的示例中那样,是否可以“OK”,还是应该在外部定义?
  • 我想了解为什么每个实例的私有变量值对原型函数不可访问,只有第一个实例的值可以访问。

闭包再次出现... - Dormilich
2
阅读闭包(http://www.jibbering.com/faq/faq_notes/closures.html)以了解您的代码为什么会表现出这样的行为。 - outis
5个回答

23
什么情况下使用原型函数比闭包更有意义呢?这是最轻量级的方法。假设您在某个构造函数的原型中有一个方法,并且创建了1000个对象实例,那些对象将在它们的原型链中具有该方法,并且所有这些对象都将引用一个函数对象。如果在构造函数内初始化该方法,例如(this.method = function () {};),则您的1000个对象实例将具有函数对象作为自己的属性。如果出于某种原因需要使用原型函数,像我的例子一样在类内部定义是否“可以”,或者应该在外部定义?在构造函数的原型中定义其成员本身并没有太大意义,我将向您解释更多关于此的内容,以及为什么您的代码无法工作。让我们看看您的代码:
var Container = function(param) {
    this.member = param;
    var privateVar = param;
    if (!Container.prototype.stamp) {  // <-- executed on the first call only
        Container.prototype.stamp = function(string) {
            return privateVar + this.member + string;
        }
    }
}
你的代码行为的关键点在于 Container.prototype.stamp 函数是在第一次调用该方法时创建的。
当你创建函数对象时,它会将当前封闭作用域存储在内部属性 [[Scope]] 中。
稍后,通过使用 var 或 FunctionDeclaration 声明的标识符(变量)来调用函数,会扩充该作用域。 [[Scope]] 属性列表构成了一个作用域链,当你访问标识符(如你的 privateVar 变量)时,这些对象会被检查。
由于你的函数是在第一次方法调用 (new Container('A')) 时创建的,所以 privateVar 绑定到此第一次函数调用的 Scope 上,并且无论你如何调用该方法,它都将保持绑定状态。
请参考这个答案,其中第一部分是关于 with 语句,但在第二部分中我谈到了函数作用域链的工作原理。

@CMS:非常清晰的解释,非常感谢!所以,如果我的原型函数只访问 this 而不是私有变量,那么即使它是在类内部定义的,它也可以正常工作,对吗?与通常看到的在类之后声明原型函数相比,这样做有什么缺点吗? - Tauren
1
@Tauren,是的,存在一个缺点,即会有内存泄漏问题。例如,在您的代码中,在构造函数第一次调用时声明的所有变量都不会被垃圾回收,因为现在您已经知道,从Container.prototype.stamp函数创建的封闭作用域仍然可以在构造函数执行结束后访问(创建了一个闭包)。因此,一些库,比如Google的Closure ,避免使用闭包来实现私有成员,他们仅使用命名约定,例如 obj.__privateMember - Christian C. Salvadó

12

抱歉对于翻旧问题的复活,但我想要补充一些最近在 SO 其他地方发现的东西 (正在找链接,找到后会编辑/添加)找到了

个人而言,我喜欢下面这种方法,因为我可以将所有我的原型和“实例”定义与函数定义视觉组合在一起,同时避免多次评估它们。此外,它还提供了使用原型方法进行闭包的机会,这对于创建由不同原型方法共享的“私有”变量非常有用。

var MyObject = (function () {
    // Note that this variable can be closured with the 'instance' and prototype methods below
    var outerScope = function(){};

    // This function will ultimately be the "constructor" for your object
    function MyObject() {
        var privateVariable = 1; // both of these private vars are really closures specific to each instance
        var privateFunction = function(){};
        this.PublicProtectedFunction = function(){ };
    }

    // "Static" like properties/functions, not specific to each instance but not a prototype either
    MyObject.Count = 0;

    // Prototype declarations
    MyObject.prototype.someFunction = function () { };
    MyObject.prototype.someValue = 1;

    return MyObject;
})(); 

// note we do automatic evalution of this function, which means the 'instance' and prototype definitions 
// will only be evaluated/defined once.  Now, everytime we do the following, we get a new instance
// as defined by the 'function MyObject' definition inside

var test = new MyObject();

1

你需要将函数放在每个特定实例上,而不是原型上,像这样:

Container = function(param) {
    this.member = param;
    var privateVar = param;

    this.stamp = function(string) {
        return privateVar + this.member + string;
    }
}

@Slaks:我已经修订了问题,更清楚地表达我所要问的内容。 - Tauren

1
为了获得您想要的行为,您需要为每个单独的对象分配独特的闭包,并分别赋予stamp()函数:
Container = function(param) {
    this.member = param;
    var privateVar = param;
    this.stamp = function(string) {
        return privateVar + this.member + string;
    }
}

当您在原型上创建单个函数时,每个对象都使用相同的函数,并且该函数会关闭第一个容器的privateVar

通过每次调用构造函数时分配this.stamp = ...,每个对象都将获得自己的stamp()函数。这是必要的,因为每个stamp()需要关闭不同的privateVar变量。


@John:谢谢你的回答。我想我的问题表述不够清晰,所以我进行了修改。请看一下修改后的内容。 - Tauren

0
这是因为privateVar不是对象的私有成员,而是stamp闭包的一部分。您可以通过始终创建该函数来获得效果:
Container = function(param) {
    this.member = param;
    var privateVar = param;
    this.stamp = function(string) {
      return privateVar + this.member + string;
    }
}

privateVar的值在函数构建时设置,因此每次需要创建它。

编辑:修改以不设置原型。


@Kathy:谢谢,我同意使用闭包。请看我的编辑过的问题,因为我增加了更多关于我想要问的内容的细节。 - Tauren
@Tauren 变量仅被设置为第一个私有变量的原因是该函数仅创建一次,因此闭包也仅被创建一次。 - Kathy Van Stone
注意:在这种情况下,由于每次构造函数运行时都会重新声明stamp函数,因此privateVar(在stamp方法中可访问)将始终引用任何实例的最后一个实例化对象范围内的值。 - Christian C. Salvadó
@CMS - 你说得对 -- 我会修改代码直接在此处设置时间戳。 - Kathy Van Stone

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接