Javascript 行为委托模式中的可变隐私

Variable privacy in Javascript's behaviour delegation pattern

自从 以来,我一直在研究 Javascript 的原型模型,并试图摆脱我从其他 继承 的 OOP 愿景语言(双关语)。

我回到基础并阅读了 Crookford 的 Javascript: The Good Parts, along with You Don't Know JS material 并决定坚持所谓的 行为委托

重构我之前的实施行为委托和命名空间的示例,我写道:

var GAME = {};

(function(namespace) {
    var Warrior = {};

    Warrior.init = function(weapon) {
        this.setWeapon(weapon);
    };

    Warrior.getWeapon = function() {
      return this.weapon;
    };

    Warrior.setWeapon = function(value) {
      this.weapon = value || "Bare hands";
    };

    namespace.Warrior = namespace.Warrior || Warrior;
})(GAME);

(function(namespace) {
    var Archer = Object.create(namespace.Warrior);

    Archer.init = function(accuracy) {
        this.setWeapon("Bow");
        this.setAccuracy(accuracy);
    };

    Archer.getAccuracy = function() {
        return this.accuracy;
    };

    Archer.setAccuracy = function(value) {
      this.accuracy = value;
    };

    namespace.Archer = namespace.Archer || Archer;
})(GAME);

所以,每次我复制一个新的 Archer 对象时:

var archer1 = Object.create(GAME.Archer);

只会创建这个对象,节省内存。

但是如果我不想公开 "accuracy" 属性怎么办?该属性只会通过调用 "training()" 方法或类似方法来增加。我尝试在匿名函数中使用 var accuracy,但它变成了一种静态变量,Archer 的所有实例都将共享相同的值。

问题:有没有办法在保持 behaviour-delegation/prototypal 模式的同时将变量设置为私有?

我也知道功能模式,在这里我以内存为代价成功实现了变量隐私。通过运行,每个新的 "archer" 实例都会生成一个新的 "Warrior",然后是一个新的 "Archer"。即使考虑到 Chrome 和 Firefox 有不同的优化,对两者的测试报告 Delegation/Prototypal 模式更有效:

http://jsperf.com/delegation-vs-functional-pattern

如果我采用纯对象委托模式,我是否应该忘记经典的封装概念并接受属性的自由变化性质?

我会尝试用与告诉您如何使用库的 略有不同的内容来回答您的问题。 不同之处在于它(希望)会给你一些想法,让你知道我们如何自己解决 OLOO 中的私有变量问题。至少在某种程度上,使用我们自己的代码,不需要外部库,这在某些情况下可能会有用。


为了使代码更清晰,我已经对您的匿名包装函数进行了条带化​​处理,因为它们与问题没有任何关系。

var Warrior = {};

Warrior.warInit = function (weapon){
   this.setWeapon(weapon);    
}

Warrior.getWeapon = function(){
   return this.weapon;
}

Warrior.setWeapon = function (value){
   this.weapon = value || "Bare hands";
}

var Archer = Object.create(Warrior);

Archer.archInit = function (accuracy){
   this.setWeapon("Bow");   
   this.setAccuracy(accuracy); 
}

Archer.getAccuracy = function (pocket) {
   return pocket.accuracy;
}
Archer.setAccuracy = function (value, pocket){
   pocket.accuracy = value;
}

function attachPocket(){ 

   var pocket = {};

   var archer = Object.create(Archer); 

   archer.getAccuracy = function(){
      var args = Array.prototype.slice.call(arguments);
      args = args.concat([pocket]); 

      return Archer.getAccuracy.apply(this, args)
   }; 
   archer.setAccuracy = function(){ 
      var args = Array.prototype.slice.call(arguments);
      args = args.concat([pocket]); 

      return Archer.setAccuracy.apply(this, args);
   }; 

   return archer;
}


var archer1 = attachPocket();  

archer1.archInit("accuracy high"); 
console.log(archer1.getAccuracy()); // accuracy high
archer1.setAccuracy("accuracy medium");
console.log(archer1.getAccuracy()); // accuracy medium

上面的测试代码here。 (并打开浏览器控制台)

用法

1 ) OLOO 中关于在原型链的不同级别上命名函数的一般做法与 OOP 是恰当的。我们希望 不同的 名称更具描述性和自记录性,从而使代码更清晰、更易读。更重要的是,通过给出不同的名称,我们避免了递归循环:

Archer.init = function(accuracy, pocket){
     this.init() // here we reference Archer.init() again, indefinite recurson. Not what we want 
      ...
}
Archer.archInit = fucntion (accuracy, pocket){ // better,
     this.warInit() // no name "collisions" .
}

2 ) 我们已经创建了一个 attachPocket() 函数来创建内部变量 pocket。使用 Object.create() 创建新对象并将其原型设置为指向 Archer。暂停。如果您注意到,需要我们定义的私有变量的函数,以便它们每个都多接受一个参数(pocket),有些只使用 pocket。这是诀窍。

By making wrapper functions like archer.setAccuracy(), archer.getAccuracy() ... we can create closures and call directly functions that need private var (here pocket) and pass it to them as an argument.

像这样:

 function AtachPocket(){
   ...
   var pocket = {};

   archer.setAccuracy = function(){ 
     var args = Array.prototype.slice.call(arguments);
     args = args.concat([pocket]); // appending pocket to args
     return Archer.setAccuracy(this, args); 
   }; 
   ...
 }

从本质上讲,通过这样做,我们绕过了在原型链中正常搜索函数的过程,只搜索需要私有变量的函数。这就是"call directly"所指的。 通过为 archer("instance") 中的函数设置与它在原型链 (Archer) 中相同的名称,我们在实例级别隐藏了该函数。没有无限循环的危险,因为我们是 "calling directly" 如上所述。此外,通过使用相同的函数名称,我们保留了在 "instance" 中访问相同函数的正常预期行为,就像在原型链中一样。这意味着在 var archer = Object.create(Archer) 之后我们可以访问函数 setAccuracy 就像我们在原型链中正常搜索函数一样。

3 ) 每次调用 attachPocket() 时,它都会创建一个新的 "instance",其中包含那些传递口袋参数的包装函数(全部作为内部细节执行)。因此每个实例都有自己的、唯一的、私有变量。

您通常会在 "instance" 中使用函数:

archer1.archInit("accuracy high"); // Passing needed arguments.
                               // Placed into pocked internally.
archer1.getAccuracy(); // Getting accuracy from pocket.

可扩展性

到目前为止,我们所拥有的只是 "attaches a pocket" 具有 Archer.setAccuracyArcher.getAccuracy 等硬编码值的函数。如果我们想通过引入像这样的新对象类型 var AdvancedArcher = Object.create(Archer) 来扩展原型链怎么办,如果我们将 AdvancedArcher 对象传递给 attachPocket 甚至可能没有的对象,它会如何表现setAccuracy()函数?每次我们在原型链中引入一些变化时,我们都要改变 attachPocket() 吗?

让我们尝试通过使 attachPocket() 更通用来回答这些问题。


首先,扩展原型链。

var AdvancedArcher = Object.create(Archer);

AdvancedArcher.advInit = function(range, accuracy){
    this.archInit(accuracy);
    this.setShotRange(range);
}
AdvancedArcher.setShotRange = function(val){
    this.shotRange = val;
}

更通用 attachPocket

function attachPocketGen(warriorType){

   var funcsForPocket = Array.prototype.slice.call(arguments,1); // Take functions that need pocket
   var len = funcsForPocket.length; 

   var pocket = {};
   var archer = Object.create(warriorType); // Linking prototype chain

   for (var i = 0; i < len; i++){ // You could use ES6 "let" here instead of IIFE below, for same effect
      (function(){  
         var func = funcsForPocket[i]; 
         archer[func] = function(){ 
             var args = Array.prototype.slice.call(arguments);
             args = args.concat([pocket]); // appending pocket to args

             return warriorType[func].apply(this, args);
         }
      })()
   }

   return archer;
}

 var archer1 = attachPocketGen(Archer,"getAccuracy","setAccuracy");  

archer1.advInit("11","accuracy high"); 
console.log(archer1.getAccuracy()); // "accuracy high";

archer1.setAccuracy("accuracy medium");
console.log(archer1.getAccuracy());

测试代码here

在这个更通用的 attachPocketGen 作为第一个参数中,我们有一个 warriorType 变量代表我们原型链中的任何对象。可以闲置的参数是那些代表需要私有变量的函数名称的参数。

attachPocketGen 获取这些函数名称并在 archer "instance" 中创建具有相同名称的包装函数。阴影,就像以前一样。 另一件需要认识到的事情是,这种制作包装函数并使用 apply() 函数从闭包传递变量的模型将适用于仅使用 pocket 的函数、使用 pocket 和其他变量的函数,当然,这些变量使用它们前面的相对 this 引用。 所以我们已经实现了一些更好用的 attachPocket,但这仍然是应该注意的事情。

1) 通过必须传递需要私有变量的函数名称,这种用法意味着我们(attachPocketGen 用户)需要知道整个原型链(所以我们可以看到哪些函数需要私有变量)。因此,如果你要像这里那样制作一个原型链,只需将 attachPocketGen 作为 API 传递给想要使用你的行为委托和私有变量的程序员,he/she 将不得不分析原型链中的对象。有时这不是我们想要的。

1a) 但我们可以改为在原型链中定义我们的函数时(如 Archer.getAccuracy)向它们添加一个 属性 就像一个标志可以判断该函数是否需要私有变量:

Archer.getAccuracy.flg = true;

然后我们可以添加额外的逻辑来检查原型链中具有此 flg 并填充 funcsForPocket 的所有函数。 结果将是只有这个电话:

var archer1 = attachPocketGen(AdvancedArcher)

warriorType 外没有其他参数。使用此函数的用户无需知道原型链的样子,这就是函数需要私有变量的原因。

改进的样式

如果我们要看这段代码:

Archer.archInit = function (accuracy){
   this.setWeapon("Bow");   
   this.setAccuracy(accuracy); 
}

我们看到它使用了"pocket"函数setAccuracy。但是我们没有在这里添加 pocket 作为它的最后一个参数,因为被调用的 setAccuracy 是影子版本,来自实例的那个。因为它 仅从这样的实例调用 archer1.archInit(...) 那个已经添加了一个口袋作为最后一个参数,所以有必要。这有点不错,但它的定义是:

Archer.setAccuracy = function (value, pocket){ ...

这在像上面的 Archer.archInit 这样的原型链中创建对象时可能会造成混淆。如果我们查看 setAccuracy 的定义,看起来我们应该这样做。因此,为了不必记住我们 不必添加 口袋作为使用其他口袋函数的函数(如 archInit)中的最后一个参数,也许我们应该做些什么像这样:

Archer.setAccuracy = function (value){  
   var pocket = arguments[arguments.length -1];

   pocket.accuracy = value;
}

测试代码here

没有口袋作为函数定义中的最后一个参数。现在很明显,不必在代码中的任何地方以 pocket 作为参数调用函数。

1 ) 当我们考虑更通用的原型链和 attachPocketGen 时,还有其他可以说是次要的东西需要参考。就像当我们不想给他们传递一个 pocket,即将 pocket 用法切换到一个函数,但为了不让这个 post 太长,让我们暂停一下。

希望这可以为您提供解决问题的思路。