简体   繁体   English

使私有实例变量可以被匿名函数中包含的原型方法访问

[英]Making private instance variable accessible to prototype methods enclosed in anonymous function

Background 背景

I decided I would practice by making a simple calculator app in JS. 我决定通过在JS中制作一个简单的计算器应用程序来练习。 The first step was to implement a stack class. 第一步是实现堆栈类。 I ran into some problems however in achieving data encapsulation with the revealing prototype pattern (?). 然而,在使用显示原型模式(?)实现数据封装时遇到了一些问题。 Here's how it looks right now: 以下是它现在的样子:

Stack "class": 堆栈“类”:

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;

})();

Test code: 测试代码:

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());

The Problem 问题

The only problem is that the arr is accessible. 唯一的问题是arr是可访问的。 I would like to hide the arr variable somehow. 我想以某种方式隐藏arr变量。

Attempts at a Solution 尝试解决方案

My first idea was to make it a private variable like Stack : 我的第一个想法是使它成为像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.

})();

But of course this approach doesn't work, because then the arr variable is shared by every instance. 但是当然这种方法不起作用,因为每个实例都会共享 arr变量。 So it's a good way of making a private class variable, but not a private instance variable. 因此,它是创建私有变量的好方法,但不是私有实例变量。

The second way I thought of (which is really crazy and definitely not good for readability) is to use a random number to restrict access to the array variable, almost like a password: 我想到的第二种方式(真的很疯狂,绝对不能提高可读性)是使用随机数来限制对数组变量的访问,几乎就像密码一样:

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.

})();

This solution is... amusing. 这个解决方案很有趣。 But obviously not what I want to do. 但显然不是我想要做的。

The last idea, which is what Crockford does , allows me to create a private instance member, but there's no way I can tell to make this visible to the public prototype methods I'm defining. 最后一个想法,就是Crockford所做的 ,允许我创建一个私有实例成员,但是我无法告诉我这对于我定义的公共原型方法是否可见。

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] This is almost there, but I don't want to have the function definitions within the var Stack = function() {...} because then they get recreated every time that an instance is created. [1]这几乎就在那里,但我不想在var Stack = function() {...}使用函数定义,因为每次创建实例时都会重新创建它们。 A smart JS compiler will realize that they don't depend on any conditionals and cache the function code rather than recreating this.push over and over, but I'd rather not depend on speculative caching if I can avoid it. 一个聪明的JS编译器会意识到它们不依赖于任何条件并缓存函数代码而不是this.push遍地重新创建this.push ,但如果我可以避免它,我宁愿不依赖于推测性缓存。

The Question 问题

Is there a way to create a private instance member which is accessible to the prototype methods? 有没有办法创建一个可供原型方法访问的私有实例成员? By somehow utilizing the 'bubble of influence' created by the enclosing anonymous function? 通过某种方式利用封闭匿名函数创建的“影响泡沫”?

You could use a factory function that creates an instance for you: 您可以使用为您创建实例的工厂函数:

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();

}

You would be defining the class on every execution of the factory function, but you could get around this by changing this to define most of Stack outside the constructor function, like the parts that dont use arr could be further up the prototype chain. 您将在每次执行工厂函数时定义类,但是您可以通过更改此函数来定义构造函数外部的大部分Stack ,就像不使用arr的部分可以进一步在原型链中一样。 Personally I use Object.create instead of prototype now and I almost always use factory functions to make instances of these types of objects. 我个人现在使用Object.create而不是原型,我几乎总是使用工厂函数来创建这些类型的对象的实例。

Another thing you could do is maintain a counter that keeps track of the instance and holds on to an array of arrays. 您可以做的另一件事是维护一个跟踪实例的计数器并保持一个数组数组。

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.

}());

Now you have the hidden data multi dimensional array, and every instance just maintains its index in that array. 现在您拥有隐藏数据多维数组,并且每个实例只在该数组中维护其索引。 You have to be careful to manage the memory now though, so that when your instance isn't being used anymore you remove what's in that array. 现在你必须小心管理内存,这样当你的实例不再被使用时你会删除该数组中的内容。 I don't recommend doing it this way unless you are disposing your data carefully. 除非您小心处理数据,否则我不建议这样做。

A real solution 真正的解决方案

EDIT: It turns out this solution is basically the same as the one described here , first posted by HMR in a comment to my question above. 编辑:事实证明这个解决方案基本上与这里描述的解决方案相同,首先由HMR在我上面的问题的评论中发布。 So definitely not new, but it works well. 所以绝对不是新的,但它运作良好。

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]

Cons: basically none, other than remembering a little extra code 缺点:基本上没有,除了记住一些额外的代码

Original solution 原始解决方案

(The first method originally posted was substantially different from what is below, but through some careless editing I seem to have lost it. It didn't work as well anyway, so no real harm done.) (最初发布的第一种方法与下面的方法大不相同,但是通过一些粗心的编辑,我似乎已经失去了它。无论如何它都没有用,所以没有真正的伤害。)

Here a new object/prototype is created with every instantiation, but it borrows much of the code from the static privilegedInstanceMethods . 这里使用每个实例创建一个新的对象/原型,但它借用了static privilegedInstanceMethods中的大部分代码。 What still fails is the ability to do Stack.prototype.push.call(s1, val) , but now that the prototype is being set on the object, I think we're getting closer. 仍然失败的是能够做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;

}());

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"]

Pros: 优点:

  • this.arr is not directly accessible this.arr无法直接访问
  • method code is only defined once, not per instance 方法代码只定义一次,而不是每个实例
  • s1.push(x) works and so does Stack.prototype.push.call(s1, x) s1.push(x)工作, Stack.prototype.push.call(s1, x)

Cons: 缺点:

  • The bind call creates four new wrapper functions on every instantiation (but the code is much smaller than creating the internal push/pop/empty/size functions every time). bind调用在每个实例化上创建了四个新的包装器函数(但是代码比每次创建内部push / pop / empty / size函数要小得多)。
  • The code is a little complicated 代码有点复杂

The short answer here, is that you can't have all things, without sacrificing a little. 这里简短的回答是,你不能在没有牺牲一点的情况下拥有所有东西。

A Stack feels like a struct of some kind, or at very least, a data-type which should have either a form of peek or read-access, into the array. Stack感觉就像某种类型的struct ,或者至少是一种数据类型,它应该具有一种peek形式或读取访问形式。

Whether the array is extended or not, is of course up to you and your interpretation... 阵列是否扩展,当然取决于您和您的解释......

...but my point is that for low-level, simple things like this, your solution is one of two things: ...但我的观点是,对于像这样的低级,简单的事情,你的解决方案是两件事之一:

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

or 要么

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 here is just a simple function that you can write, to copy the key->val of objects, onto the first object (basically, so I don't have to keep typing this. or Class.prototype. . extend这里只是,你可以写一个简单的函数,来复制对象的键- > VAL,到第一个对象(基本上,这样我就不必继续键入this.Class.prototype.

There are, of course, dozens of ways of writing these, which will all achieve basically the same thing, with modified styles. 当然,有很多种方法可以用改进的样式来编写这些方法,这些方法都可以实现基本相同的功能。

And here's the rub; 这就是摩擦; unless you do use a global registry, where each instance is given its own unique Symbol (or unique-id) at construction time, which it then uses to register an array... ...which of course, means that the key then needs to be publicly accessible (or have a public accessor -- same thing), you're either writing instance-based methods, instance-based accessors with prototyped methods, or you're putting everything you need in the public scope. 除非你使用全局注册表,其中每个实例在构造时被赋予其自己唯一的Symbol(或unique-id),然后它用于注册数组......当然,这意味着密钥然后需要公开访问(或具有公共访问者 - 同样的事情),您要么编写基于实例的方法,使用原型方法编写基于实例的访问器,要么将所需内容放在公共范围内。

In the future, you will be able to do things like this: 将来,您将能够做到这样的事情:

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;
}());

Nearly all bleeding-edge browsers support this, currently (minus the shorthand for methods). 目前几乎所有前沿的浏览器都支持这一点(减去方法的简写)。
But there are ES6 -> ES5 compilers out there (Traceur, for instance). 但是有ES6 - > ES5编译器(例如Traceur)。
I don't think WeakMaps are supported in Traceur, as an ES5 implementation would require a lot of hoops, or a working Proxy, but a Map would work (assuming that you handled GC yourself). 我不认为Traceur支持WeakMaps,因为ES5实现需要很多环节或者工作代理,但Map可以工作(假设您自己处理GC)。

This lends me to say that from a pragmatic standpoint, for a class as small as Stack you might as well just give each instance its own methods, if you really want to keep the array internal. 这让我说从一个实用的角度来看,对于像Stack这样小的类,如果你真的想让数组保持内部,你可以给每个实例提供自己的方法。

For other harmless, tiny, low-level classes, hiding data might be pointless, so all of it could be public. 对于其他无害的,微小的,低级别的类,隐藏数据可能毫无意义,因此所有这些都可能是公开的。

For larger classes, or high-level classes, having accessors on instances with prototyped methods stays relatively clean; 对于较大的类或高级类,在具有原型方法的实例上具有访问器保持相对干净; especially if you're using DI to feed in lower-level functionality, and the instance accessors are just bridging from the interface of the dependency, into the shape you need them to be, for your own interface. 特别是如果您使用DI来提供较低级别的功能,并且实例访问器只是从依赖关系的接口桥接到您需要它们的形状,对于您自己的接口。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM