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

时间:2017-03-05 17:29:05

标签: javascript ecmascript-6 stampit.js

我试图围绕对象链接其他对象来编写节点模块。这是我到目前为止(灵感来自this answer):

'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可能会有更优雅的方式。我主要担心的是,我认为我应该在儿童工厂做let parent = Object.create(Parent),但如果我这样做,那就不行了。

所以,1)我错过了什么,2)可以重构吗?

4 个答案:

答案 0 :(得分:6)

你绝对应该比单一祖先类继承更喜欢组合(包括mixins),所以你就是在正确的轨道上。也就是说,JavaScript并不具备私有属性,因为您可能从其他语言中了解它们。我们在JS中使用闭包来保护数据隐私。

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

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

标记是一个可组合的工厂函数,它根据描述符返回对象实例。邮票有一个名为.compose()的方法。调用.compose()方法时,使用当前标记作为基础创建新标记,由作为参数传递的可组合列表组成:

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

可组合是一个标记或POJO(普通旧JavaScript对象)标记描述符。

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

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

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

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

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

  • compose(…composables: [...Composable]) => Stamp接受任意数量的可组合并返回一个新标记。
  • init(…functions: [...Function]) => Stamp接受任意数量的初始化函数并返回一个新标记。

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

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'

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

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

  1. 组合不是类继承。不要试图建模 - 关系或根据父母/子女关系思考事物。相反,使用基于特征的思维。 myNewObject需要featureAfeatureBfeatureC,因此:myNewFactory = compose(featureA, featureB, featureC); myNewObject = myNewFactory()。请注意,myNewObject不是featureAfeatureB等的实例 ...而是实现使用,或包含这些功能。
  2. 邮票& mixins不应该彼此了解。 (没有隐式依赖)。
  3. 邮票& mixins应该很小。引入尽可能少的新属性。
  4. 在作曲时,你可以而且应该只选择性地继承你需要的道具,并重命名道具以避免碰撞。
  5. 无论何时都可以使用模块进行代码重用(大部分时间都应如此)。
  6. 首选域模型和状态管理的函数式编程。避免共享可变状态。
  7. 首选高阶函数和高阶组件而不是任何类型的继承(包括mixin或stamps)。
  8. 如果你坚持这些准则,你的邮票和& mixins将不受常见遗传问题的影响,例如脆弱的基类问题,大猩猩/香蕉问题,必要性问题的重复等等......

答案 1 :(得分:1)

使用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财产并非偶然。通常_命名约定(可能是非可枚举的描述符)足以将成员指定为私有/受保护。

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

答案 2 :(得分:1)

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

为了完全理解所提供的示例,我做了重构,更明确地命名了不同的可组合部分,尤其是“行为”部分。我也对孩子与父母的关系以及如何出口工厂感到不舒服。但由于这些例子大多是简化代码,因此必须经常猜测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; }

下一个给定的示例代码采用刚刚提供的重构示例,但使用基于函数而不是基于对象的mixins和工厂,它们创建基于类的类型而不是基于普通对象(文字)的类型。然而,两个例子都有关于如何处理封装和组合的一个相同的方法......

// 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; }

答案 3 :(得分:-1)

为了完成起见,使用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