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

时间:2014-10-04 16:21:09

标签: javascript prototype private-members

背景

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

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;

})();

测试代码:

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变量是每个实例的共享。因此,它是制作私有变量的好方法,但不是私有实例变量。

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

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,允许我创建一个私有实例成员,但是我无法告诉他们这对于公共原型方法是否可见I&#39; m定义

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,但如果我能避免的话,我宁愿不依赖于推测性缓存它

问题

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

3 个答案:

答案 0 :(得分: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.

}());

现在您拥有隐藏数据多维数组,并且每个实例都只在该数组中维护其索引。现在你必须小心管理内存,这样当你的实例不再被使用时你会删除该数组中的内容。除非您小心处理数据,否则我不建议这样做。

答案 1 :(得分:1)

真正的解决方案

编辑:事实证明这个解决方案与描述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函数要小得多。)
  • 代码有点复杂

答案 2 :(得分:1)

这里简短的回答是,你不能在不牺牲一点的情况下拥有所有东西。

Stack感觉就像某种类型的struct,或者至少是一种数据类型,它应该具有peek形式的“{1}”或读取权限。 。

阵列是否扩展,当然取决于您和您的解释......

...但我的观点是,对于像这样的低级,简单的事情,你的解决方案是两件事之一:

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-&gt; val复制到第一个对象上(基本上,所以我不必一直打字{{ 1}}或this.

当然,有很多种方法可以用改进的风格来编写这些方法,这些方法基本上都是一样的。

这就是摩擦;除非你使用全局注册表,其中每个实例在构造时被赋予其自己唯一的Symbol(或unique-id),然后它用于注册数组......当然,这意味着密钥然后需要公开访问(或具有公共访问者 - 同样的事情),您要么编写基于实例的方法,使用原型方法编写基于实例的访问器,要么将您需要的所有内容放在公共场所范围。

将来,您将能够做到这样的事情:

Class.prototype.

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

这让我说从一个实用的角度来看,对于像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; }()); 一样小的类,如果你真的想让数组保持内部,你可以给每个实例提供自己的方法。

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

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