6

在这里,我发现了一种启用对象私有成员的 javascript 模块模式。如果我做对了,它可以这样写:

var myObject1 = (function(){
  var privateMember = 42;
  return {
    publicMember: function() {
      return privateMember;
    }
  }
})();

但是有更有效的方法:

var myObject2 = new function() {
  var privateMember = 42;
  this.publicMember = function() {
    return privateMember;
  }
}

这两者有什么区别吗?还有其他实现私有成员的可能性吗?

这是 Chrome 调试器的结果:

在此处输入图像描述

4

2 回答 2

5

这两者之间只有几个真正的技术差异(Bergi 在评论中指出)。除非你做成千上万个这样的事情,否则这些差异可能并不重要。存在风格差异,这是主观的,但技术差异很小。有关血腥细节,请参阅下面的细分。

在高层次上,这两种形式都创建了一个新对象,它们都依赖于私有成员的闭包。第二种方式使用更多的内存,因为函数在使用后不能被垃圾收集,而在第一种方式中,它可以。同样,创建为函数prototype属性的对象在第一个版本中是可回收的,但不是第二个版本。对于某些人来说,第二种方式也可能比第一种方式不太清楚(见证下面来自@djechlin 的问题),尤其是因为隐式返回值,但这是一个风格点。

关于你的第二个问题:

还有其他实现私有成员的可能性吗?

对于像你这样的一次性对象,你这样做的方式可能是最好的(真的是任何一种形式)。但是现在,有另一种方式,从 ES.Next(ECMAScript 的下一个版本)开始,有一种新的真正私有的方式。但是,在这两种情况下,当你做对象类而不是一次性的时,它更有用。

在我的博客上的这篇文章中有更多内容(其中也谈到了 ES.Next 附带的东西),但我将在这里介绍最重要的细节。

对象类中私有成员的典型模式如下所示:

function Foo(toBePrivate) {
    var privateMember = toBePrivate;

    this.method1 = function() {
        // ...do something with the truly private `privateMember`
    };
}
Foo.prototype.method2 = function() {
    // ...doesn't have access to the truly private `privateMember`
};

var f = new Foo("private stuff");
// f.method1() can use the private data
// f.method2() cannot

它的问题是通过创建的每个对象new Foo都有自己的 method1功能。我们没有得到我们得到的重用method2,其中只有一个由 .创建的所有对象共享new Foo。(这对现代引擎来说不一定是什么大问题,即使为每个对象创建一个新对象,它们也能够重用代码。但是有一些动态更改原型的开发模式,显然无法行动在上面。)method1method1 Foomethod1

这是我们今天可以做的近乎私有的模式:

var Foo = (function() {
    // Create a private name object for our private property
    var privateKey = makeRandomString();

    // Our constructor    
    function Foo(toBePrivate) {
        this[privateKey] = toBePrivate;
    }

    // Define the private property so it's non-enumerable
    Object.defineProperty(Foo.prototype, privateKey, {
        writable: true
    });

    // Methods shared by all Foo instances
    Foo.prototype.method1 = function() {
        // ...use this[privateKey] here...
    };
    Foo.prototype.method2 = function() {
        // ...use this[privateKey] here...
    };

    return Foo;
})();

var f = new Foo("private stuff");
// f.method1() can use the private data
// f.method2() can too!
// Both `method1` and `method2` are *reused* by all `Foo` objects

......makeRandomString正是这样做的,每次我们调用它时它都会给我们一个新的随机字符串。

我们创建的属性不是私有的,但它确实是晦涩难懂的。它不会出现在for-in循环中(因为我们创建的属性是不可枚举的),并且每次代码运行时它的名称都会改变。因此,任何尝试使用私有数据的代码都必须首先找出属性名称,这是一个非常重要的练习,因为该代码无法获取对象的不可枚举属性名称的列表。不过,很自然地,在调试器中看一眼对象就会向您显示属性及其值。该物业确实晦涩难懂,但并非真正私密。

ES.Next(ECMAScript 的下一个版本)中的内容显着改进了这种模式。我们将获得私有名称对象,它们本身很有用,也被类使用。

以下是私有名称如何应用于上述内容:

// **ES.Next, not yet available in the wild**
import Name from "@name";
var Foo = (function() {
    // Create a private name object for our private property
    var privateKey = new Name();

    function Foo(toBePrivate) {
        this[privateKey] = toBePrivate;
    }
    Foo.prototype.method1 = function() {
        // ...use this[privateKey] here...
    };
    Foo.prototype.method2 = function() {
        // ...use this[privateKey] here...
    };

    return Foo;
})();

var f = new Foo("private stuff");
// f.method1() can use the private data
// f.method2() can too!
// Both `method1` and `method2` are *reused* by all `Foo` objects

使用私有名称对象创建的属性根本不会出现在for-in枚举,它们的名称也不是字符串。如果没有特定的名称对象,代码就无法访问名称为私有名称对象的属性。由于上面的变量对类是完全私有的,因此没有其他代码可以使用该属性。它是完全私人的。自然,这些将出现在调试器中,但是,调试器没有什么是私有的。privateKeyFoo


@djechlin 要求详细说明您的每个私人会员表格的工作方式,这有助于我们了解 Bergi 强调的它们之间的区别:

你的第一个例子:

var myObject1 = (function(){
  var privateMember = 42;
  return {
    publicMember: function() {
      return privateMember;
    }
  }
})();

(这份清单遗漏了一些我认为不相关的细节。)

  1. myObject1在当前变量绑定对象(如果 this 是全局的)上创建一个名为的属性window,其值为undefined

  2. 评估函数表达式:
    A)Function要创建的对象。
    B) 创建一个空白对象并将其分配给新函数的prototype属性。
    C)constructor在该对象上创建一个属性并给出对该函数的引用。
    D) 对当前变量绑定对象的引用存储在函数中。

  3. 调用该函数,为调用的执行上下文创建(除其他外)一个变量绑定对象。

  4. 创建一个名为的属性privateMember并将其分配给步骤 3 中的变量绑定对象。

  5. 值 42 分配给privateMemberVBO 的属性。

  6. 一个空白对象被创建并被赋予了一个原型Object.prototype

  7. 评估内部函数表达式,Function创建一个对象(其prototype属性为空白对象,该对象上放置一个constructor属性,并引用当前变量绑定对象 [步骤 3 中的那个])。

  8. 该函数从步骤 5 分配给空白对象上的属性,如publicMember.

  9. 从主匿名函数返回对第 6 步中对象的引用。

  10. 该对象引用存储在myObject1步骤 1 中创建的属性中。

  11. 主匿名函数(来自第 2 步)没有未完成的引用,因此可以被 GC 回收;因此其prototype属性所引用的对象也可以被 GC 回收。

你的第二个例子:

var myObject2 = new function() {
  var privateMember = 42;
  this.publicMember = function() {
    return privateMember;
  }
}

(同样,一些不相关的细节被遗漏了。)

  1. myObject2在当前变量绑定对象(如果 this 是全局的)上创建一个名为的属性window,其值为undefined

  2. 评估函数表达式:
    A)Function要创建的对象。
    B) 创建一个空白对象并将其分配给新函数的prototype属性。
    C)constructor在该对象上创建一个属性并给出对该函数的引用。
    D) 对当前变量绑定对象的引用存储在函数中。

  3. 创建一个新的空白对象,并根据匿名函数的prototype属性为其分配原型。

  4. 调用该函数时将步骤 3 中的对象作为 传入,为this调用的执行上下文创建(除其他外)一个变量绑定对象。

  5. 创建一个名为的属性privateMember并将其分配给步骤 4 中的变量绑定对象。

  6. 值 42 分配给privateMemberVBO 的属性。

  7. 评估内部函数表达式,Function创建一个对象(其prototype属性为空白对象,该对象上放置一个constructor属性,并引用当前变量绑定对象 [步骤 4 中的那个])。

  8. 该函数从步骤 5 分配给空白对象上的属性,如publicMember.

  9. 该函数返回,因为它不返回对象,所以new表达式的结果是对在步骤 3 中创建的对象的引用。

  10. 该对象引用存储在myObject2步骤 1 中创建的属性中。

  11. 主匿名函数(来自第 2 步)不能被 GC 回收,因为myObject2的底层原型在属性上有对其的引用constructor(因此分配给其prototype属性的函数和对象都保留在内存中)。

您可以通过在其中添加以下行来释放函数(但不是分配给其prototype属性的对象):

delete this.constructor.prototype.constructor;

prototype这将从分配给其属性的对象中删除对该函数的引用。该对象仍然是myObject2的底层原型,但它不再引用该函数,因此该函数符合 GC 条件。

但在那一点上,我们已经进入了默默无闻的领域。:-)

结论

所以它们几乎是一样的,只是主匿名函数和它的prototype属性上的对象不符合 GC 的条件。在现实世界中,这将需要成千上万的这些。

(旁注:一些实现可能会推迟创建函数的一些步骤——例如为其prototype属性创建一个空白对象并设置它constructor ——直到/除非prototype使用该属性,因为当然在绝大多数情况下,它是从未使用过,因为绝大多数函数从未用作构造函数。因此,您的第一个表单可能会稍微高效一点,因为它可以跳过这些步骤。除非您做数千次,否则这种差异不太可能发生这些。)


FWIW,第一个也可以这样写,如果你关心的是行数,括号数,或者不喜欢对象字面量等:

var myObject1 = function(){
  var obj = {};
  var privateMember = 42;
  obj.publicMember = function() {
    return privateMember;
  };
  return obj;
}();

作为风格问题,我更喜欢显式返回,但这是风格问题。

于 2013-05-14T21:17:24.690 回答
2

这两者有什么区别吗?

一个主要区别是,在使用new的第二种情况下,返回的对象在其[[Prototype]]链上还有一个附加对象,即构造函数表达式的公共原型。该对象可以在某些浏览器中使用__proto__.

另外,myObject1.constructor是内置的 Object 函数,whilemyObject2.constructor是函数表达式创建的函数。

还有其他实现私有成员的可能性吗?

我想你可以使用对象的(否则无用的)__proto__对象,但这比隐私更容易混淆(并且在某些浏览器中不可用):

var foo = new function() {

  this.__proto__.privateMember = 42;

  this.publicMember = function() {
    return this.__proto__.privateMember;
  }
};

alert(foo.publicMember());

真的很丑。

闭包被用来在 javascript 中模拟私有成员已经有一段时间了(至少超过 10 年),但并没有真正的压力将它们包含在 ECMA-262 中(据我所知)。我想这表明私有成员很方便,但对于实现 javascript 通常用于的主机环境脚本类型所需的功能并不重要。

编辑

正如 Bergi 所说,应该使用 ES5 Object.getPrototypeOf方法代替(非标准)__proto__属性。我最初没有指出这一点是不好的。

于 2013-05-14T22:44:28.350 回答