如何在 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
不是错别字。重点是证明 a
和 b
的私有变量不会冲突。
但真正的享受是:
const c = compose(a, b);
const foo = c();
console.log(foo.getA()); // 'a'
console.log(foo.getB()); // 'b'
瓦特?是的。您只是同时从两个来源继承了特权方法和私有数据。
在使用可组合对象时应遵守一些经验法则:
- 组合不是class继承。不要尝试建模 is-a 关系或根据 parent/child 关系来思考事物。相反,使用基于特征的思维。
myNewObject
需要 featureA
、featureB
和 featureC
,所以:myNewFactory = compose(featureA, featureB, featureC); myNewObject = myNewFactory()
。请注意 myNewObject
不是 featureA
、featureB
等的 实例 ... 相反,它 实现 、使用,或包含这些功能。
- Stamp 和 mixins 不应该相互了解。 (没有隐式依赖)。
- Stamp 和 mixins 应该很小。引入尽可能少的新属性。
- 在合成时,您可以而且应该有选择地只继承您需要的道具,并重命名道具以避免冲突。
- 只要有可能(大多数时候应该是这样),优先使用模块进行代码重用。
- 更喜欢领域模型和状态管理的函数式编程。避免共享可变状态。
- 优先使用高阶函数和高阶组件,而不是任何类型的继承(包括 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; }
我正在尝试围绕对象链接其他对象来编写 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
不是错别字。重点是证明 a
和 b
的私有变量不会冲突。
但真正的享受是:
const c = compose(a, b);
const foo = c();
console.log(foo.getA()); // 'a'
console.log(foo.getB()); // 'b'
瓦特?是的。您只是同时从两个来源继承了特权方法和私有数据。
在使用可组合对象时应遵守一些经验法则:
- 组合不是class继承。不要尝试建模 is-a 关系或根据 parent/child 关系来思考事物。相反,使用基于特征的思维。
myNewObject
需要featureA
、featureB
和featureC
,所以:myNewFactory = compose(featureA, featureB, featureC); myNewObject = myNewFactory()
。请注意myNewObject
不是featureA
、featureB
等的 实例 ... 相反,它 实现 、使用,或包含这些功能。 - Stamp 和 mixins 不应该相互了解。 (没有隐式依赖)。
- Stamp 和 mixins 应该很小。引入尽可能少的新属性。
- 在合成时,您可以而且应该有选择地只继承您需要的道具,并重命名道具以避免冲突。
- 只要有可能(大多数时候应该是这样),优先使用模块进行代码重用。
- 更喜欢领域模型和状态管理的函数式编程。避免共享可变状态。
- 优先使用高阶函数和高阶组件,而不是任何类型的继承(包括 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; }