英文:
How do I utilize Javascript closure and private variables while also using Object.assign + Object.create in a factory function?
问题
我正在尝试弄清楚如何同时使用工厂函数闭包和Object.assign/Object.create继承。可能很简单,但我不知道如何做到。首先,我建立了一个希望使用闭包的工厂。我有一些操作私有变量的方法。
const projectFactory = function() {
let _title = "new project";
const _tabs = [];
const getTitle = function() {
return _title;
}
const setTitle = function(input) {
_title = input;
return;
}
const createTab = function() {
// 一些填充tabs数组的方法
return;
}
return {
getTitle,
setTitle,
createTab
}
};
const factory1 = projectFactory();
console.log(factory1); // 方法设置在对象上,_title和_tabs不可见
好吧,我创建的所有这些对象在调用此工厂函数时,所有方法都将相同。所以我决定将我的方法提取出来,存储在一个对象中供引用,然后重写我的工厂函数使用Object.assign/Object.create,这样方法就会被继承。
const projectMethods = {
getTitle() {
return this.title;
},
setTitle(input) {
this.title = input;
return;
},
createTab() {
// 一些填充tabs数组的方法
return;
}
};
const projectFactory = function() {
let title = "new project";
const tabs = [];
return Object.assign(Object.create(projectMethods), {
title, tabs
});
};
const factory1 = projectFactory();
console.log(factory1); // 方法在原型上可用,title和tabs可见
但是现在,由于我在我的工厂中返回了一个完整的对象,我不再拥有私有变量。我如何实现以下结果:
console.log(factory1); // 方法在原型上可用,title和tabs不可见
英文:
I'm trying to figure out how to incorporate factory function closures and use Object.assign/Object.create inheritance at the same time. Probably simple, but I can't figure out how to do it. First I build my desired factory that utilizes closure. I have methods that act on my private variables.
const projectFactory = function() {
let _title = "new project";
const _tabs = [];
const getTitle = function() {
return _title;
}
const setTitle = function(input) {
_title = input;
return;
}
const createTab = function() {
// some method that fills the tabs array
return;
}
return {
getTitle,
setTitle,
createTab
}
};
const factory1 = projectFactory();
console.log(factory1); // methods set on object, _title and _tabs not visible
`
Well all my methods are going to be the same for all these objects I'm creating when calling this factory function. So I decided to pull out my methods and store them in an object to be referenced then rewrite my factory function using Object.assign / Object.create, this way the methods are inherited.
const projectMethods = {
getTitle() {
return this.title;
},
setTitle(input) {
this.title = input;
return;
},
createTab() {
// some method that fills the tabs array
return;
}
};
const projectFactory = function() {
let title = "new project";
const tabs = [];
return Object.assign(Object.create(projectMethods), {
title, tabs
});
};
const factory1 = projectFactory();
console.log(factory1); // methods available on prototype, title and tabs visible
But now since I'm returning a whole object in my factory I no longer have private variables. How can I achieve the result:
console.log(factory1); // methods available on prototype, title and tabs not visible
答案1
得分: 1
以下是您要翻译的内容:
"有一种模式实际上允许在模块化的混合功能/行为之间共享(局部封闭/私有)状态。当然,它必须完全基于闭包。因此,不太合适的是闭包和 Object.assign
结合使用,后者发生在这些闭包之外(就像在OP的示例代码中所示)。另一方面,仅使用闭包的方法已经足够了。
将这种模式应用于第一次迭代时,OP的提供的代码可以实现如下...
// 基于函数的混合,包含共享状态。
function withStatfulProjectBehavior(sharedState) {
const project = this;
function getTitle() {
return sharedState.title;
}
function setTitle(value) {
return (sharedState.title = value);
}
function createTab() {
// 一些填充tabs数组的方法...例如...
return sharedState.tabs.push({
title: 'new tab',
});
}
return Object.assign(project, {
getTitle,
setTitle,
createTab,
});
}
// `project` 工厂函数。
function createProject() {
const sharedState = {
tabs: [],
title: 'new project',
}
const project = {};
return withStatfulProjectBehavior.call(project, sharedState);
};
const project = createProject();
console.log({ project });
console.log(
'project.getTitle() ...',
project.getTitle()
);
console.log(
"project.setTitle('Foo Bar') ...",
project.setTitle('Foo Bar')
);
console.log(
'project.getTitle() ...',
project.getTitle()
);
console.log(
'project.createTab() ...',
project.createTab()
);
第二次迭代的结果可能看起来像...
// 辅助功能
function validateTitle(value) {
if (typeof value !== 'string') {
console.warn('A title has to by a string value.');
return false;
} else {
return true;
}
}
// - 基于函数的,上下文感知的混合,它在其上下文和共享状态上应用了通用实现的受保护属性访问。
function withSharedProtectedProperty(
state, { key, validation, enumerable = false }
) {
const type = this;
Reflect.defineProperty(type, key, {
get: () => state[key],
set: value => {
if (validation(value)) {
return state[key] = value;
}
},
enumerable,
});
}
// - 基于函数的,上下文感知的混合,它在其上下文和共享状态上应用了标签特定的行为。
function withSharedTabManagement(state) {
this.addTab = function addTab(title = 'unnamed tab') {
// 一些填充tabs数组的方法...例如...
return state.tabs.push({ title });
};
}
// `project` 工厂函数。
function createProject() {
const state = {
tabs: [],
title: 'new project',
}
const project = {};
withSharedProtectedProperty.call(project, state, {
enumerable: true,
key: 'title',
validation: validateTitle,
});
withSharedTabManagement.call(project, state);
return project;
};
const project = createProject();
console.log({ project });
console.log(
'project.title ...', project.title
);
console.log(
"project.title = 1234 ...", (project.title = 1234)
);
console.log(
'project.title ...', project.title
);
console.log(
"project.title = 'Foo Bar' ...", (project.title = 'Foo Bar')
);
console.log(
'project.title ...', project.title
);
console.log(
'project.addTab() ...', project.addTab()
);
您还可以查看...
编辑...为了考虑Bergi的一些建议...
"[@stuffz] ... 您不能同时拥有私有变量和共享继承方法。共享闭包是不可能的。" – Bergi
"@PeterSeliger 在实例的多个方法之间共享状态是容易的事情。不可能的是共享方法(通过原型继承)." – Bergi
...如果根据上述提到的胶水代码直接实现OP的原始示例代码,那么它应该非常接近...
// 项目特定模块范围。
// 通过`WeakMap`实例进行状态管理。
function getState(reference) {
return projectStates.get(reference);
}
const projectStates = new WeakMap;
// 基于对象的状态化(后来是原型的)项目行为混合。
const statefulProjectMethods = {
getTitle() {
return getState(this).title;
},
setTitle(input) {
return getState(this).title = input;
},
createTab(title = 'unnamed tab') {
// 一些填充tabs数组的方法...例如...
return getState(this).tabs.push({ title });
},
};
// `project`工厂函数。
/*export */function createProject(protoMethods) {
const project = Object.create(protoMethods);
projectStates.set(project, {
tabs: [],
title: 'new project',
});
return project;
};
// 项目特定模块范围的结束。
// 另一个模块的范围
// import createProject from '/project.js';
const project = createProject(statefulProjectMethods);
console.log(
"project.hasOwnProperty('getTitle') ...", project.hasOwnProperty('getTitle')
);
console.log(
"project.hasOwnProperty('setTitle') ...", project.hasOwnProperty('setTitle')
);
console.log(
"project.hasOwnProperty('createTab') ...", project.hasOwnProperty('createTab')
);
console.log(
"Object.getPrototypeOf(project) ...", Object.getPrototypeOf(project)
);
console.log(
'project.getTitle() ...', project.getTitle()
);
console.log(
"project.setTitle('Foo Bar') ...", project.setTitle('Foo Bar')
);
console.log(
'project.getTitle() ...', project.getTitle()
);
console
<details>
<summary>英文:</summary>
There is actually [a pattern which allows shared (locally enclosed / private) state amongst modularized mixin-functionality/behavior](https://stackoverflow.com/search?q=user%3A2627243+mixin+shared+state). It, of cause, has to be based exclusively on closures. Therefore the one thing which does not work well together is closures **and** `Object.assign` where the latter happens outside of such closures (like shown with the OP's example code). On the other hand a closures-only approach already is sufficient enough.
Applying such a pattern to a first iteration, the OP's presented code could be implemented like ...
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
// function based mixin which incorporates shared state.
function withStatfulProjectBehavior(sharedState) {
const project = this;
function getTitle() {
return sharedState.title;
}
function setTitle(value) {
return (sharedState.title = value);
}
function createTab() {
// some method that fills the tabs array ... e.g. ...
return sharedState.tabs.push({
title: 'new tab',
});
}
return Object.assign(project, {
getTitle,
setTitle,
createTab,
});
}
// `project` factory function.
function createProject() {
const sharedState = {
tabs: [],
title: 'new project',
}
const project = {};
return withStatfulProjectBehavior.call(project, sharedState);
};
const project = createProject();
console.log({ project });
console.log(
'project.getTitle() ...',
project.getTitle()
);
console.log(
"project.setTitle('Foo Bar') ...",
project.setTitle('Foo Bar')
);
console.log(
'project.getTitle() ...',
project.getTitle()
);
console.log(
'project.createTab() ...',
project.createTab()
);
<!-- language: lang-css -->
.as-console-wrapper { min-height: 100%!important; top: 0; }
<!-- end snippet -->
And a 2nd iteration's outcome then already could look like ...
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
// helper functionality
function validateTitle(value) {
if (typeof value !== 'string') {
// throw new TypeError('A title has to by a string value.');
console.warn('A title has to by a string value.');
return false;
} else {
return true;
}
}
// - function based, context aware mixin which applies generically
// implemented protected property access at its context and over
// shared state.
function withSharedProtectedProperty(
state, { key, validation, enumerable = false }
) {
const type = this;
Reflect.defineProperty(type, key, {
get: () => state[key],
set: value => {
if (validation(value)) {
return state[key] = value;
}
},
enumerable,
});
}
// - function based, context aware mixin which applies tab
// specific behavior at its context and over shared state.
function withSharedTabManagement(state) {
this.addTab = function addTab(title = 'unnamed tab') {
// some method that fills the tabs array ... e.g. ...
return state.tabs.push({ title });
};
}
// `project` factory function.
function createProject() {
const state = {
tabs: [],
title: 'new project',
}
const project = {};
withSharedProtectedProperty.call(project, state, {
enumerable: true,
key: 'title',
validation: validateTitle,
});
withSharedTabManagement.call(project, state);
return project;
};
const project = createProject();
console.log({ project });
console.log(
'project.title ...', project.title
);
console.log(
"project.title = 1234 ...", (project.title = 1234)
);
console.log(
'project.title ...', project.title
);
console.log(
"project.title = 'Foo Bar' ...", (project.title = 'Foo Bar')
);
console.log(
'project.title ...', project.title
);
console.log(
'project.addTab() ...', project.addTab()
);
<!-- language: lang-css -->
.as-console-wrapper { min-height: 100%!important; top: 0; }
<!-- end snippet -->
One also might have a look at ...
- [Sharing state when applying Douglas Crockford's composition pattern](https://stackoverflow.com/a/70037962/2627243)
- [How to properly replace 'extends', using functional programming?](https://stackoverflow.com/a/61688193/2627243)
- [How would one implement generic functionality which gets applied across various distinct classes without inheritance?](https://stackoverflow.com/a/76267031/2627243)
_**Edit** ... in order to take into account some of **Bergi's** comments ..._
> _"[@stuffz] ... You cannot have private variables and shared inherited methods at the same time. It's just impossible to have a shared closure." – Bergi_
> _"@PeterSeliger Sharing state between multiple methods of an instance is the easy thing. What is not possible is sharing the methods (via prototype inheritance)." – Bergi_
... where one of my replied sentences stated ...
> _"... (Yet, a `Map` or `WeakMap` based implementation with just a little glue-code should be able to process local/private state via prototypal methods.)" – Peter Seliger_
... any direct implementation of the OP's original example code, based on the above mentioned glue-code, then should be very close to ...
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
// project specific module scope.
// state management via `WeakMap` instance.
function getState(reference) {
return projectStates.get(reference);
}
const projectStates = new WeakMap;
// object based mixin of stateful (later prototypal) project behavior.
const statefulProjectMethods = {
getTitle() {
return getState(this).title;
},
setTitle(input) {
return getState(this).title = input;
},
createTab(title = 'unnamed tab') {
// some method that fills the tabs array ... e.g. ...
return getState(this).tabs.push({ title });
},
};
// `project` factory function.
/*export */function createProject(protoMethods) {
const project = Object.create(protoMethods);
projectStates.set(project, {
tabs: [],
title: 'new project',
});
return project;
};
// end of project specific module scope.
// another module's scope
// import createProject from '/project.js';
const project = createProject(statefulProjectMethods);
console.log(
"project.hasOwnProperty('getTitle') ...", project.hasOwnProperty('getTitle')
);
console.log(
"project.hasOwnProperty('setTitle') ...", project.hasOwnProperty('setTitle')
);
console.log(
"project.hasOwnProperty('createTab') ...", project.hasOwnProperty('createTab')
);
console.log(
"Object.getPrototypeOf(project) ...", Object.getPrototypeOf(project)
);
console.log(
'project.getTitle() ...', project.getTitle()
);
console.log(
"project.setTitle('Foo Bar') ...", project.setTitle('Foo Bar')
);
console.log(
'project.getTitle() ...', project.getTitle()
);
console.log(
'project.createTab() ...', project.createTab()
);
<!-- language: lang-css -->
.as-console-wrapper { min-height: 100%!important; top: 0; }
<!-- end snippet -->
</details>
# 答案2
**得分**: -1
I've translated the code parts for you:
```js
**state monad**
Maybe you're looking for the state monad. The first example in Peter's post is similar to how the state monad works. The primary difference is state changes do not mutate previous state.
Let's first design the initial state of our project -
```js
const initProject = {
title: "<insert title>",
tabs: [{ title: "Untitled.txt" }]
}
Now we can write a function that updates the title -
const setTitle = title =>
State.get().bind(project => // get the state, bind to "project"
State.put({...project, title}) // set new state
)
Here's one to create a new tab -
const createTab = title =>
State.get().bind(project => // get the state, bind to "project"
State.put({...project, tabs: [...project.tabs, {title}]}) // set new state
)
The bind
function of any monad allows us to read the contained value, perform some computation with it, and return a new monad encapsulating the result of that computation -
console.log(
setTitle("hello world")
.bind(() => createTab("2"))
.bind(() => createTab("3"))
.execState(initState)
)
{
"title": "hello world",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "2"
},
{
"title": "3"
}
]
}
Let's run a code example to check our progress. Don't worry about understanding State
for now -
// state monad
const State = Object.assign(
runState => ({
runState,
bind: f => State(s => {
let {value, state} = runState(s)
return f(value).runState(state)
}),
evalState: s => runState(s).value,
execState: s => runState(s).state,
}),
{
return: y => State(x => ({value: y, state: x})),
get: () => State(x => ({value: x, state: x})),
put: x => State(_ => ({value: null, state: x})),
},
)
// your methods
const setTitle = title =>
State.get().bind(project =>
State.put({...project, title})
)
const createTab = title =>
State.get().bind(project =>
State.put({...project, tabs: [...project.tabs, {title}]})
)
// your program
const initState = {
title: "<insert title>",
tabs: [{ title: "Untitled.txt" }],
}
console.log(
setTitle("hello world")
.bind(() => createTab("2"))
.bind(() => createTab("3"))
.execState(initState)
)
console.log(
createTab("⚠️")
.bind(() => createTab("⚠️"))
.execState(initState)
)
.as-console-wrapper { min-height: 100%; top: 0 }
```js
{
"title": "hello world",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "2"
},
{
"title": "3"
}
]
}
{
"title": "<insert title>",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "⚠️"
},
{
"title": "⚠️"
}
]
}
too much .bind!
The .bind(project => ...)
allows you to read the state, similar to how Promise .then(value => ...)
allows you to read the value of a promise, but these closures can be a burden to work with. Much like Promise has async..await
, we can implement State.run
to eliminate need for .bind
closures -
const setTitle = title => State.run(function *() {
const project = yield State.get() // State.get().bind(project => ...
return State.put({...project, title})
})
const createTab = title => State.run(function *() {
const project = yield State.get() // State.get().bind(project => ...
return State.put({...project, tabs: [...project.tabs, {title}]})
})
The benefit is observed when more .bind
calls are saved. If the result of the bind is not needed, you can leave the LHS of yield
empty -
State.run(function *() {
yield setTitle("hello world") // setTitle("hello world").bind(() => ...
yield createTab("2") // createTab("2").bind(() => ...
return createTab("3")
})
This updated demo using State.run
produces the same result without the need for .bind
closures -
// state monad
const State = Object.assign(
runState => ({
runState,
bind: f => State(s => {
let {value, state} = runState(s)
return f(value).runState(state)
}),
evalState: s => runState(s).value,
execState: s => runState(s).state,
}),
{
return: y => State(x => ({value: y, state: x})),
get: () => State(x => ({value: x, state: x})),
put: x => State(_ => ({value: null, state: x})),
run: e => {
const g = e()
const next = x => {
let {done, value} = g.next(x)
return done ? value : value.bind(next)
}
return next()
},
},
)
// your methods
const setTitle = title => State.run(function *() {
const project = yield State.get()
return State.put({...project, title})
})
const createTab = title => State.run(function *() {
const project = yield State.get()
return State.put({...project, tabs: [...project.tabs, {title}]})
})
// your program
const initProject = {
title: "<insert title>",
tabs: [{ title: "Untitled.txt" }],
}
console.log(State.run(function *() {
yield setTitle("hello world")
yield createTab("2")
return createTab("3")
}).execState(initProject))
console.log(State.run(function *() {
yield createTab("⚠️")
return createTab("⚠️")
}).execState(initProject))
.as-console-wrapper { min-height: 100%; top: 0 }
英文:
state monad
Maybe you're looking for the state monad. The first example in Peter's post is similar to how the state monad works. The primary difference is state changes do not mutate previous state.
Let's first design the initial state of our project -
const initProject = {
title: "<insert title>",
tabs: [{ title: "Untitled.txt" }]
}
Now we can write a function that updates the title -
const setTitle = title =>
State.get().bind(project => // get the state, bind to "project"
State.put({...project, title}) // set new state
)
Here's one to create a new tab -
const createTab = title =>
State.get().bind(project => // get the state, bind to "project"
State.put({...project, tabs: [...project.tabs, {title}]}) // set new state
)
The bind
function of any monad allows us to read the contained value, perform some computation with it, and return a new monad encapsulating the result of that computation -
console.log(
setTitle("hello world")
.bind(() => createTab("2"))
.bind(() => createTab("3"))
.execState(initState)
)
{
"title": "hello world",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "2"
},
{
"title": "3"
}
]
}
Let's run a code example to check our progress. Don't worry about understanding State
for now -
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
// state monad
const State = Object.assign(
runState => ({
runState,
bind: f => State(s => {
let {value, state} = runState(s)
return f(value).runState(state)
}),
evalState: s => runState(s).value,
execState: s => runState(s).state,
}),
{
return: y => State(x => ({value: y, state: x})),
get: () => State(x => ({value: x, state: x})),
put: x => State(_ => ({value: null, state: x})),
},
)
// your methods
const setTitle = title =>
State.get().bind(project =>
State.put({...project, title})
)
const createTab = title =>
State.get().bind(project =>
State.put({...project, tabs: [...project.tabs, {title}]})
)
// your program
const initState = {
title: "<insert title>",
tabs: [{ title: "Untitled.txt" }],
}
console.log(
setTitle("hello world")
.bind(() => createTab("2"))
.bind(() => createTab("3"))
.execState(initState)
)
console.log(
createTab("⚠️")
.bind(() => createTab("⚠️"))
.execState(initState)
)
<!-- language: lang-css -->
.as-console-wrapper { min-height: 100%; top: 0 }
<!-- end snippet -->
{
"title": "hello world",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "2"
},
{
"title": "3"
}
]
}
{
"title": "<insert title>",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "⚠️"
},
{
"title": "⚠️"
}
]
}
too much .bind!
The .bind(project => ...)
allows you to read the state, similar to how Promise .then(value => ...)
allows you to read the value of a promise, but these closures can be a burden to work with. Much like Promise has async..await
, we can implement State.run
to eliminate need for .bind
closures -
const setTitle = title => State.run(function *() {
const project = yield State.get() // State.get().bind(project => ...
return State.put({...project, title})
})
const createTab = title => State.run(function *() {
const project = yield State.get() // State.get().bind(project => ...
return State.put({...project, tabs: [...project.tabs, {title}]})
})
The benefit is observed when more .bind
calls are saved. If the result of the bind is not needed, you can leave the LHS of yield
empty -
State.run(function *() {
yield setTitle("hello world") // setTitle("hello world").bind(() => ...
yield createTab("2") // createTab("2").bind(() => ...
return createTab("3")
})
This updated demo using State.run
produces the same result without the need for .bind
closures -
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
// state monad
const State = Object.assign(
runState => ({
runState,
bind: f => State(s => {
let {value, state} = runState(s)
return f(value).runState(state)
}),
evalState: s => runState(s).value,
execState: s => runState(s).state,
}),
{
return: y => State(x => ({value: y, state: x})),
get: () => State(x => ({value: x, state: x})),
put: x => State(_ => ({value: null, state: x})),
run: e => {
const g = e()
const next = x => {
let {done, value} = g.next(x)
return done ? value : value.bind(next)
}
return next()
},
},
)
// your methods
const setTitle = title => State.run(function *() {
const project = yield State.get()
return State.put({...project, title})
})
const createTab = title => State.run(function *() {
const project = yield State.get()
return State.put({...project, tabs: [...project.tabs, {title}]})
})
// your program
const initProject = {
title: "<insert title>",
tabs: [{ title: "Untitled.txt" }],
}
console.log(State.run(function *() {
yield setTitle("hello world")
yield createTab("2")
return createTab("3")
}).execState(initProject))
console.log(State.run(function *() {
yield createTab("⚠️")
return createTab("⚠️")
}).execState(initProject))
<!-- language: lang-css -->
.as-console-wrapper { min-height: 100%; top: 0 }
<!-- end snippet -->
{
"title": "hello world",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "2"
},
{
"title": "3"
}
]
}
{
"title": "<insert title>",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "⚠️"
},
{
"title": "⚠️"
}
]
}
related
You may find other useful details about the state monad in an older post.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论