如何在 JS 中使用行为委托(OLOO)定义私有变量?

How to define private variables with behavior delegation (OLOO) in JS?

我正在尝试围绕对象链接其他对象来编写 Node 模块。这是我目前所拥有的(受 启发):

'use strict'

// Composable prototype object
var parent = {
  publicVar: 1,
  doSomething() {
    return externalMethod(this.publicVar) + 10
  }
}

// Composable prototype object
var child = {
  doSomethingChild() {
    return this.publicVar + 20
  } 
}

// an external method
function externalMethod(arg) {
  return arg
}

// the parent factory
function Parent() {
  let privateVar = 2

  return Object.assign({
    getPrivate() {
      return privateVar
    }
  }, parent)
}

// the child factory
function Child() {
  let privateVar = 4
  let parent = Parent() // call to the Parent factory
  return Object.assign(parent, child, {
    getPrivateChild() {
        return privateVar
    }
  })
}

// Node export
module.exports = {
  Parent: Parent(),
  Child: Child()
}

稍后,我将需要这样的模块:

Parent = require('./my-module').Parent
Child = require('./my-module').Child
Parent.getPrivate() // 2
Parent.doSomething() // 11
Child.getPrivateChild() // 4
Child.doSomethingChild() // 21

恐怕使用 OLOO 可能有更优雅的方法来执行此操作。我主要担心的是,我认为我应该在 Child 工厂中做 let parent = Object.create(Parent),但如果我这样做了,那是行不通的。

所以,1) 我是否遗漏了什么,2) 可以重构吗?

有了ES6类,就和

一样简单
class Parent {
  constructor() {
    this.publicVar = 1;
    this._privateVar = 2;
  }

  getPrivate() {
    return this._privateVar;
  }

  doSomething() {
    return externalMethod(this.publicVar) + 10
  }
}

class Child extends Parent {
  constructor() {
    super();
    this._privateVar = 4;
  }

  doSomethingChild() {
    return this.publicVar + 20
  } 
}

module.exports = {
  parent: new Parent(),
  child: new Child()
}

根据 publicVar_privateVar 的角色,它们可能是静态属性。

_privateVar 属性 的使用并非偶然。通常 _ 命名约定(可能还有不可枚举的描述符)足以将成员指定为 private/protected.

Object.assign作为ES6中的主要继承技术是无效的,但可以额外使用来实现多态继承。

您绝对应该更喜欢组合(包括混合)而不是单祖先 class 继承,所以您走在正确的轨道上。也就是说,JavaScript 没有您可能从其他语言了解到的私有属性。我们在 JS 中使用闭包来保护数据隐私。

对于具有真实数据隐私(通过闭包)的可组合原型,您正在寻找的是功能性混合,它们是接受对象和return的函数添加了新功能的对象。

但是,在我看来,使用可组合工厂进行功能继承通常是更好的做法(例如 stamps). AFAIK, Stampit 是最广泛使用的可组合工厂实现。

邮票是一个可组合的工厂函数,returns 对象实例基于其描述符。邮票有一种叫做 .compose() 的方法。调用 .compose() 方法时,使用当前图章作为基础创建新图章,由作为参数传递的可组合项列表组成:

const combinedStamp = baseStamp.compose(composable1, composable2, composable3);

可组合项是印记或 POJO(普通旧 JavaScript 对象)印记描述符。

.compose() 方法兼作邮票的描述符。换句话说,描述符属性附加到 stamp .compose() 方法,例如stamp.compose.methods.

可组合描述符(或简称描述符)是一个元数据对象,其中包含创建对象实例所需的信息。 描述符包含:

  • methods — 将添加到对象的委托原型的一组方法。
  • properties — 将通过赋值添加到新对象实例的一组属性。
  • initializers — 一个函数数组,将运行顺序排列。标记详细信息和参数传递给初始化器。
  • staticProperties — 一组静态属性,将通过赋值复制到图章。

基本问题,如“我如何继承特权方法和私有数据?”以及“继承层次结构有哪些好的替代方案?”是许多 JavaScript 用户的难点。

让我们使用 stamp-utils 库中的 init()compose() 同时回答这两个问题。

  • compose(…composables: [...Composable]) => Stamp 接受任意数量的可组合项和 return 一个新图章。
  • init(…functions: [...Function]) => Stamp 采用任意数量的初始化函数和 return 一个新戳记。

首先,我们将使用闭包来创建数据隐私:

const a = init(function () {
  const a = 'a';

  Object.assign(this, {
    getA () {
      return a;
    }
  });
});

console.log(typeof a()); // 'object'
console.log(a().getA()); // 'a'

它使用函数作用域来封装私有数据。请注意,必须在函数内部定义 getter 才能访问闭包变量。

这是另一个:

const b = init(function () {
  const a = 'b';

  Object.assign(this, {
    getB () {
      return a;
    }
  });
});

那些 a 不是错别字。重点是证明 ab 的私有变量不会冲突。

但真正的享受是:

const c = compose(a, b);

const foo = c();
console.log(foo.getA()); // 'a'
console.log(foo.getB()); // 'b'

瓦特?是的。您只是同时从两个来源继承了特权方法和私有数据。

在使用可组合对象时应遵守一些经验法则:

  1. 组合不是class继承。不要尝试建模 is-a 关系或根据 parent/child 关系来思考事物。相反,使用基于特征的思维。 myNewObject 需要 featureAfeatureBfeatureC,所以:myNewFactory = compose(featureA, featureB, featureC); myNewObject = myNewFactory()。请注意 myNewObject 不是 featureAfeatureB 等的 实例 ... 相反,它 实现 使用,或包含这些功能。
  2. Stamp 和 mixins 不应该相互了解。 (没有隐式依赖)。
  3. Stamp 和 mixins 应该很小。引入尽可能少的新属性。
  4. 在合成时,您可以而且应该有选择地只继承您需要的道具,并重命名道具以避免冲突。
  5. 只要有可能(大多数时候应该是这样),优先使用模块进行代码重用。
  6. 更喜欢领域模型和状态管理的函数式编程。避免共享可变状态。
  7. 优先使用高阶函数和高阶组件,而不是任何类型的继承(包括 mixins 或 stamp)。

如果您坚持这些准则,您的 stamp 和 mixin 将不会受到常见继承问题的影响,例如脆弱的基础 class 问题、gorilla/banana 问题、必然重复问题等...

为了完成起见,使用 Eric Elliott 的答案非常简单:

var stampit = require('stampit')

function externalMethod(myVar) {
  return myVar
}

parent = stampit().init(function({value}){ 
    this.privateVar = 2
  }).props({
    publicVar: 1
  }).methods({
    doSomething() {
      return externalMethod(this.publicVar) + 10
    },
    getPrivate() {
      return this.privateVar
    }
  })

child = parent.init(function({value}){
    this.privateVar = 4
  }).methods({
  doSomethingChild() {
    return this.publicVar + 20
  }
})

parent().getPrivate() // 2
parent().doSomething() // 11
child().getPrivate() // 4
child().doSomething() // 11
child().doSomethingChild() // 21

在仔细阅读了所有的帖子并试图理解OP的问题之后,我认为OP的方法已经非常接近可靠的解决方案了。毕竟,JS 中的可靠封装大多回退到一些基于闭包的技术。纯 object 和基于工厂的方法也很精益。

为了完全理解提供的示例,我确实重构了它,更明确地命名了不同的可组合部分,尤其是 "behavior" 部分。我也对 child-parent 关系以及如何导出工厂感到不舒服。但由于此类示例大多是浓缩代码,因此人们不得不经常猜测 OP 的现实问题。

这就是 运行 重构的代码,它确实保留了 OP 的方法,看起来像...

// an external method
function externalPassThroughMethod(value) {
  return value;
}

// some composable object based behavior
const withGetPublicValueIncrementedByTen = {
  getPublicValueIncrementedByTen() {

    return (externalPassThroughMethod(this.publicValue) + 10);
  }
};

// another composable object based behavior
const withGetPublicValueIncrementedByTwenty = {
  getPublicValueIncrementedByTwenty() {

    return (externalPassThroughMethod(this.publicValue) + 20);
  }
};

// the parent factory
function createParent(publicOptions = {}) {
  var localValue = 2;

  // `publicValue` via `publicOptions`
  return Object.assign({}, publicOptions, withGetPublicValueIncrementedByTen, {
    getLocalValue() {

      return localValue;
    }
  });
}

// the child factory
function createChild(parent) {
  var localValue = 4;

  // `publicValue` via `parent`
  return Object.assign({}, parent, withGetPublicValueIncrementedByTwenty, {
    getLocalValue() {

      return localValue;
    },
    getLocalValueOfParent() { // object linking other object ...

      return parent.getLocalValue(); // ... by forwarding.
    }
  });
}


// // Node export
// module.exports = {
//   createParent: createParent,
//   createChild : createChild
// }


// some (initial) key value pair
const initalPublicValue = { publicValue: 1 };

const parent  = createParent(initalPublicValue);
const child   = createChild(parent);

console.log('parent.getLocalValue()', parent.getLocalValue());                                      // 2
console.log('parent.getPublicValueIncrementedByTen()', parent.getPublicValueIncrementedByTen());    // 11
console.log('parent.getPublicValueIncrementedByTwenty', parent.getPublicValueIncrementedByTwenty);  // [UndefinedValue]

console.log('child.getLocalValue()', child.getLocalValue());                                        // 4
console.log('child.getLocalValueOfParent()', child.getLocalValueOfParent());                        // 2
console.log('child.getPublicValueIncrementedByTen()', child.getPublicValueIncrementedByTen());      // 11
console.log('child.getPublicValueIncrementedByTwenty', child.getPublicValueIncrementedByTwenty());  // 21
.as-console-wrapper { max-height: 100%!important; top: 0; }

下一个给出的示例代码采用刚刚提供的重构示例,但使用基于函数而不是基于 object 的 mixins 和工厂创建基于 class 的类型而不是普通的 object(文字)基于的。然而,这两个示例在如何处理封装和组合方面具有相同的方法......

// an external method
function externalPassThroughMethod(value) {
  return value;
}

// some composable function based behavior
const withGetPublicValueIncrementedByTen = (function () {
  function getPublicValueIncrementedByTen() {
    // implemented once ...
    return (externalPassThroughMethod(this.publicValue) + 10);
  }
  return function () {
    // ... shared (same implementation) code.
    this.getPublicValueIncrementedByTen = getPublicValueIncrementedByTen;
  };
}());

// another composable function based behavior
const withGetPublicValueIncrementedByTwenty = (function () {
  function getPublicValueIncrementedByTwenty() {
    // implemented once ...
    return (externalPassThroughMethod(this.publicValue) + 20);
  }
  return function () {
    // ... shared (same implementation) code.
    this.getPublicValueIncrementedByTwenty = getPublicValueIncrementedByTwenty;
  };
}());

class Parent {
  constructor(publicOptions = {}) {

    function getLocalValue() {
      return localValue;
    }
    var localValue = 2;

    // `publicValue` via `publicOptions`
    Object.assign(this, publicOptions);

    withGetPublicValueIncrementedByTen.call(this);

    this.getLocalValue = getLocalValue;
  }
}

class Child {
  constructor(parent) {

    function getLocalValue() {
      return localValue;
    }
    function getLocalValueOfParent() {  // object linking other object ...
      return parent.getLocalValue();    // ... by forwarding.
    }
    var localValue = 4;

    // `publicValue` via `parent`
    Object.assign(this, parent);

    withGetPublicValueIncrementedByTwenty.call(this);

    this.getLocalValue          = getLocalValue;
    this.getLocalValueOfParent  = getLocalValueOfParent;
  }
}

function createParent(publicOptions = {}) {
  return (new Parent(publicOptions));
}
function createChild(parent) {
  return (new Child(parent));
}


// // Node export
// module.exports = {
//   createParent: createParent,
//   createChild : createChild
// }


// some (initial) key value pair
const initalPublicValue = { publicValue: 1 };

const parent  = createParent(initalPublicValue);
const child   = createChild(parent);

console.log('parent.getLocalValue()', parent.getLocalValue());                                      // 2
console.log('parent.getPublicValueIncrementedByTen()', parent.getPublicValueIncrementedByTen());    // 11
console.log('parent.getPublicValueIncrementedByTwenty', parent.getPublicValueIncrementedByTwenty);  // [UndefinedValue]

console.log('child.getLocalValue()', child.getLocalValue());                                        // 4
console.log('child.getLocalValueOfParent()', child.getLocalValueOfParent());                        // 2
console.log('child.getPublicValueIncrementedByTen()', child.getPublicValueIncrementedByTen());      // 11
console.log('child.getPublicValueIncrementedByTwenty', child.getPublicValueIncrementedByTwenty());  // 21
.as-console-wrapper { max-height: 100%!important; top: 0; }