【问题标题】:Making private instance variable accessible to prototype methods enclosed in anonymous function使封装在匿名函数中的原型方法可以访问私有实例变量
【发布时间】:2014-10-04 16:21:08
【问题描述】:

背景

我决定通过在 JS 中制作一个简单的计算器应用程序来练习。第一步是实现一个堆栈类。然而,我在使用显示原型模式(?)实现数据封装时遇到了一些问题。这是它现在的样子:

堆栈“类”:

var Stack = (function () { 

    var Stack = function() {
        this.arr = []; // accessible to prototype methods but also to public
    };

    Stack.prototype = Object.prototype; // inherits from Object

    Stack.prototype.push = function(x) {
        this.arr.push(x);
    };

    Stack.prototype.pop = function() {
        return this.arr.length ? (this.arr.splice(this.arr.length - 1, 1))[0] : null;
    };

    Stack.prototype.size = function() {
        return this.arr.length;
    };

    Stack.prototype.empty = function() {
        return this.arr.length === 0;
    };

    return Stack;

})();

测试代码:

var s1 = new Stack();
var s2 = new Stack();

for(var j = 1, k = 2; j < 10, k < 11; j++, k++) {

  s1.push(3*j);
  s2.push(4*k);

}

console.log("s1:");
while(!s1.empty()) console.log(s1.pop());
console.log("s2:");
while(!s2.empty()) console.log(s2.pop());

问题

唯一的问题是arr 是可访问的。我想以某种方式隐藏arr 变量。

解决方案的尝试

我的第一个想法是把它变成一个私有变量,比如Stack

var Stack = (function () {

    var arr = []; // private, but shared by all instances

    var Stack = function() { };
    Stack.prototype = Object.prototype;

    Stack.prototype.push = function(x) {
        arr.push(x);
    };

    // etc.

})();

但是这种方法当然行不通,因为arr 变量是共享每个实例的。所以这是制作私有 class 变量而不是私有实例变量的好方法。

我想到的第二种方式(这真的很疯狂,而且绝对不利于可读性)是使用随机数来限制对数组变量的访问,几乎就像密码一样:

var Stack = (function() {

    var pass = String(Math.floor(Math.pow(10, 15 * Math.random()));
    var arrKey = "arr" + pass;

    var Stack = function() {
        this[arrKey] = []; // private instance and accessible to prototypes, but too dirty
    };

    Stack.prototype = Object.prototype;

    Stack.prototype.push = function(x) {
        this[arrKey].push(x);
    };

    // etc.

})();

这个解决方案……很有趣。但显然不是我想做的。

最后一个想法,也就是 Crockford does,允许我创建一个私有实例成员,但我无法告诉它让我定义的公共原型方法可见。

var Stack = (function() {

    var Stack = function() {
        var arr = []; // private instance member but not accessible to public methods
        this.push = function(x) { arr.push(x); }; // see note [1]
    }

})();

[1] 这几乎就在那里,但我不想在var Stack = function() {...} 中包含函数定义,因为每次创建实例时都会重新创建它们。一个聪明的 JS 编译器会意识到它们不依赖任何条件并缓存函数代码,而不是一遍又一遍地重新创建 this.push,但如果可以避免的话,我宁愿不依赖推测性缓存。

问题

有没有办法创建一个原型方法可以访问的私有实例成员?通过某种方式利用封闭匿名函数创建的“影响力泡沫”?

【问题讨论】:

  • 请注意,Douglas Crockford 考虑使用 })(); 狗球,并在将函数包装在括号中时推荐使用 }());youtube.com/watch?v=eGArABpLy0k
  • 你不能不为每个特权函数的每个实例创建一个闭包。这是一种代码更多但闭包更少的 protected 模式:stackoverflow.com/questions/21799353/…
  • @HMR 我现在看到我下面的新答案与您链接的答案基本相同。很遗憾,您第一次发帖时我没听懂(本可以为自己节省一些时间!)。

标签: javascript prototype private-members


【解决方案1】:

您可以使用为您创建实例的工厂函数:

function createStack() {
    var arr = [];

    function Stack() {

    };

    Stack.prototype = Object.prototype; // inherits from Object

    Stack.prototype.push = function(x) {
        arr.push(x);
    };

    Stack.prototype.pop = function() {
        return arr.length ? (this.arr.splice(this.arr.length - 1, 1))[0] : null;
    };

    Stack.prototype.size = function() {
        return arr.length;
    };

    Stack.prototype.empty = function() {
        return arr.length === 0;
    };

    return new Stack();

}

您将在每次执行工厂函数时定义该类,但您可以通过更改它以在构造函数之外定义大部分 Stack 来解决这个问题,例如不使用 arr 的部分可能会更进一步原型链。我个人现在使用Object.create 而不是原型,而且我几乎总是使用工厂函数来创建这些类型对象的实例。

您可以做的另一件事是维护一个计数器,该计数器跟踪实例并保存数组数组。

var Stack = (function() {

    var data = [];


    var Stack = function() {
        this.id = data.length;
        data[this.id] = [];
    };

    Stack.prototype = Object.prototype;

    Stack.prototype.push = function(x) {
        data[this.id].push(x);
    };

    // etc.

}());

现在你有了隐藏数据多维数组,每个实例都只维护它在该数组中的索引。但是,您现在必须小心管理内存,以便在不再使用您的实例时删除该数组中的内容。除非您谨慎处理数据,否则我不建议您这样做。

【讨论】:

    【解决方案2】:

    这里的简短回答是,你不可能拥有所有东西而不牺牲一点。

    Stack 感觉就像是某种struct,或者至少是一种数据类型,它应该具有peek 的形式或对数组具有读取访问权限。

    数组是否扩展,当然取决于你和你的解释......

    ...但我的意思是,对于像这样的低级简单的事情,您的解决方案是以下两件事之一:

    function Stack () {
        this.arr = [];
        this.push = function (item) { this.arr.push(item); }
        // etc
    }
    

    function Stack () {
        var arr = [];
        var stack = this;
    
        extend(stack, {
            _add  : function (item) { arr.push(item); },
            _read : function (i) { return arr[i || arr.length - 1]; },
            _remove : function () { return arr.pop(); },
            _clear  : function () { arr = []; }
        });
    }
    
    extend(Stack.prototype, {
        push : function (item) { this._add(item); },
        pop  : function () { return this._remove(); }
        // ...
    });
    

    extend 这里只是一个你可以编写的简单函数,将对象的 key->val 复制到第一个对象上(基本上,所以我不必继续输入 this.Class.prototype. .

    当然,有几十种写这些的方法,它们都可以实现基本相同的东西,只是修改了样式。

    这就是问题所在;除非您确实使用全局注册表,其中每个实例在构造时都被赋予其自己的唯一符号(或唯一 ID),然后它使用它来注册一个数组... ...当然,这意味着密钥然后需要可公开访问(或具有公共访问器——同样的事情),您要么编写基于实例的方法,使用原型方法的基于实例的访问器,要么将所需的一切置于公共范围内。

    在未来,你将能够做这样的事情:

    var Stack = (function () {
        var registry = new WeakMap();
    
        function Stack () {
            var stack = this,
                arr   = [];
    
            registry[stack] = arr;
        }
    
        extend(Stack.prototype, {
            push (item) { registry[this].push(item); }
            pop () { return registry[this].pop(); }
        });
    
        return Stack;
    }());
    

    目前,几乎所有前沿浏览器都支持此功能(减去方法的简写)。
    但是有 ES6 -> ES5 编译器(例如 Traceur)。
    我认为 Traceur 不支持 WeakMaps,因为 ES5 实现需要大量的箍或工作代理,但 Map 可以工作(假设您自己处理 GC)。

    这让我想说,从实用的角度来看,对于像 Stack 这样小的类,如果您真的想将数组保留在内部,您不妨只为每个实例提供其自己的方法。

    对于其他无害的、微小的、低级的类,隐藏数据可能毫无意义,因此所有数据都可以公开。

    对于较大的类或高级类,在具有原型方法的实例上使用访问器保持相对干净;特别是如果您使用 DI 来提供较低级别的功能,并且实例访问器只是从依赖项的接口桥接成您需要它们的形状,用于您自己的接口。

    【讨论】:

    • 哇,感谢您提供非常详细的答案。我对 WeakMap 不熟悉,所以我需要一点时间来消化它。顺便说一句,我刚刚更新了上面的答案,它似乎(至少在表面上)实现了我所希望的。 arr 是隐藏的,push 方法可以通过它们都可以通过s.push(x)Stack.prototype.push.call(s, x) 访问。
    • WeakMap 会很棒。基本上,使用Map,而不是{ str : val },对于传统对象,任何值都可以用作键{ myObj : val }myMap[myObj] === valWeakMap 做同样的事情,但是当一个对象被 GCed 时,它也会从 WeakMap 中删除(或者,作为 key/val 存在并不排除它被 GCed),而使用传统 Map(或object/array),如果该对象还包含在map/set/obj/arr中,则无法清理。
    • 在没有WeakMap 的情况下求解基本上看起来像var stack = this, arr = [], id = UUID(); registry[id] = arr; this.id = id;,并且每种方法都会更改为registry[this.id].pop(); 等。但是当然,这意味着您需要找到一种方法来告诉注册表删除数组,当您完成堆栈时... ...就像您在堆栈上手动调用的 .destroy 方法(以免它留在内存中),而 WeakMap 将停止关心堆栈/array 实例,如果它不再存在。
    • 另外,你的解决方案相当聪明——我已经做了很多——但你不想拥有基于实例的 getter/setter 的原因特别是内存开销(和技术-杂质)这样做。使用绑定或基于闭包的解决方案,每个实例最终将创建比访问器更多的方式(尤其是在 Chrome 中,如果它们都在构造函数中预先定义)。不要误会我的意思,我试图成为一个功能型的人(部分应用程序、thunk、基于集合的原子过程等)而不是试图让所有 JS 感觉像 C#/Java,但它伴随着特定的成本。
    • Proxy 可用时,我认为它在这种东西上也会非常棒,与WeakMap 结合起来实际上非常适合这种事情。这对WeakMap 很有用。我认为 WeakMap 的 pollyfill 是不可能的。它必须自动进行垃圾回收。
    【解决方案3】:

    一个真正的解决方案

    编辑:事实证明,这个解决方案与here 描述的解决方案基本相同,HMR 在对我上面的问题的评论中首次发布。所以绝对不是新的,但它运作良好。

    var Stack = (function Stack() {
    
        var key = {};
    
        var Stack = function() {
            var privateInstanceVars = {arr: []};
            this.getPrivateInstanceVars = function(k) {
                return k === key ? privateInstanceVars : undefined;
            };
        };
    
        Stack.prototype.push = function(el) {
            var privates = this.getPrivateInstanceVars(key);
            privates.arr.push(el);
        };
    
        Stack.prototype.pop = function() {
            var privates = this.getPrivateInstanceVars(key);
            return privates.arr.length ? privates.arr.splice(privates.arr.length - 1, 1)[0] : null;
        };
    
        Stack.prototype.empty = function() {
            var privates = this.getPrivateInstanceVars(key);
            return privates.arr.length === 0;
        };
    
        Stack.prototype.size = function() {
            var privates = this.getPrivateInstanceVars(key);
            return privates.arr.length;
        };
    
        Stack.prototype.toString = function() {
            var privates = this.getPrivateInstanceVars(key);
            return privates.arr.toString();
        };
    
        Stack.prototype.print = function() {
            var privates = this.getPrivateInstanceVars(key);
            console.log(privates.arr);
        }
    
        return Stack;
    
    }());
    
    
    // TEST
    
    // works - they ARE separate now
    var s1 = new Stack();
    var s2 = new Stack();
    s1.push("s1a");
    s1.push("s1b");
    s2.push("s2a");
    s2.push("s2b");
    s1.print(); // ["s1a", "s1b"]
    s2.print(); // ["s2a", "s2b"]
    
    // works!
    Stack.prototype.push.call(s1, "s1c");
    s1.print(); // ["s1a", "s1b", "s1c"]
    
    
    // extending the Stack
    
    var LimitedStack = function(maxSize) {
    
      Stack.apply(this, arguments);
      this.maxSize = maxSize;
    
    }
    
    LimitedStack.prototype = new Stack();
    LimitedStack.prototype.constructor = LimitedStack;
    
    LimitedStack.prototype.push = function() {
    
      if(this.size() < this.maxSize) {
        Stack.prototype.push.apply(this, arguments);
      } else {
        console.log("Maximum size of " + this.maxSize + " reached; cannot push.");
      }
    
      // note that the private variable arr is not directly accessible
      // to extending prototypes
      // this.getArr(key) // !! this will fail (key not defined)
    
    };
    
    var limstack = new LimitedStack(3);
    
    limstack.push(1);
    limstack.push(2);
    limstack.push(3);
    limstack.push(4); // Maximum size of 3 reached; cannot push
    limstack.print(); // [1, 2, 3]
    

    缺点:基本上没有,除了记住一些额外的代码

    原解决方案

    (最初发布的第一种方法与下面的方法有很大不同,但由于一些粗心的编辑,我似乎丢失了它。无论如何它都没有那么好,所以没有造成真正的伤害。)

    这里每次实例化都会创建一个新对象/原型,但它借用了静态privilegedInstanceMethods 的大部分代码。仍然失败的是执行Stack.prototype.push.call(s1, val) 的能力,但现在原型已设置在对象上,我认为我们越来越接近了。

    var Stack = (function() {
    
        var privilegedInstanceMethods = {
            push: function(x) { 
                this.arr.push(x);
            },
            pop: function() {
                return this.arr.length ? this.arr.splice(this.arr.length - 1, 1)[0] : null;
            },
            size: function() {
                return this.arr.length;
            },
            empty: function() {
                return this.arr.length === 0;
            },
            print: function() {
                console.log(this.arr);
            },
        };
    
        var Stack_1 = function() {
            var Stack_2 = function() {
                var privateInstanceMembers = {arr: []};
                for (var k in privilegedInstanceMethods) {
                    if (privilegedInstanceMethods.hasOwnProperty(k)) {
                        // this essentially recreates the class each time an object is created,
                        // but without recreating the majority of the function code
                        Stack_2.prototype[k] = privilegedInstanceMethods[k].bind(privateInstanceMembers);
                    }
                }
            };
            return new Stack_2(); // this is key
        };
    
        // give Stack.prototype access to the methods as well.
        for(var k in privilegedInstanceMethods) {
            if(privilegedInstanceMethods.hasOwnProperty(k)) {
                Stack_1.prototype[k] = (function(k2) {
                    return function() {
                        this[k2].apply(this, arguments);
                    };
                }(k)); // necessary to prevent k from being same in all
            }
        }
    
        return Stack_1;
    
    }());
    

    测试:

    // works - they ARE separate now
    var s1 = new Stack();
    var s2 = new Stack();
    s1.push("s1a");
    s1.push("s1b");
    s2.push("s2a");
    s2.push("s2b");
    s1.print(); // ["s1a", "s1b"]
    s2.print(); // ["s2a", "s2b"]
    
    // works!
    Stack.prototype.push.call(s1, "s1c");
    s1.print(); // ["s1a", "s1b", "s1c"]
    

    优点:

    • this.arr 不可直接访问
    • 方法代码只定义一次,而不是每个实例
    • s1.push(x) 有效,Stack.prototype.push.call(s1, x) 也有效

    缺点:

    • bind 调用在每次实例化时创建四个新的包装函数(但代码比每次创建内部 push/pop/empty/size 函数要小得多)。
    • 代码有点复杂

    【讨论】:

    • 这不起作用,s1 和 s2 在那里共享相同的东西。看看:jsfiddle.net/btipling/59p8fs3g
    • 这行得通:jsfiddle.net/btipling/59p8fs3g/1 不过我会考虑更多。
    • 我认为这是一个有趣的想法,但它并没有给你任何你无法通过 defineProperties 参数得到的东西。你基本上只是在制作一个不可写的属性,你已经可以用defineProperties 做更少的工作。这样做的副作用是privilegedInstanceMethods 中的this 与堆栈实例中的this 不同,因此它们无法共享对值的访问。如果您想要数组功能的子集,这可能很有用,但我想不出一个像样的应用程序。
    • 您基本上已经使您的实例成为第二个独立对象方法的代理。似乎您可以消除复杂性,创建该事物的新实例并将其分配给堆栈并将其作为属性引用,而不是直接代理方法。
    • 我 +1,我喜欢你的思维方向。 :D
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-03
    • 1970-01-01
    • 1970-01-01
    • 2011-12-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多