JS 中的组合
Composition in JS
我正在学习 JS 中的组合概念。下面是我的演示代码。
moveBy
函数将值正确分配给 x
和 y
。
然而,setFillColor
函数并没有将传递的值赋值给fillColor
。
调用 setFillColor
函数时究竟发生了什么?
const withMoveBy = (shape) => ({
moveBy: (diffX, diffY) => {
shape.dimensions.x += diffX;
shape.dimensions.y += diffY;
},
});
const withSetFillColor = (shape) => ({
setFillColor: (color) => {
console.log(shape.fillColor); // 1
shape.fillColor = color;
shape.dimensions.fillColor = color;
console.log(shape.fillColor); // 2
},
});
const shapeRectangle = (dimensions) => ({
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const shapeCircle = (dimensions) => ({
type: 'circle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
let shape = null;
switch (type) {
case 'rectangle': {
shape = shapeRectangle(dimensions);
break;
}
case 'circle': {
shape = shapeCircle(dimensions);
break;
}
}
if (shape) {
shape = {
...shape,
...withSetFillColor(shape),
...withMoveBy(shape),
};
}
return shape;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
let c = createShape('circle', { x: 10, y: 10, diameter: 10 });
r.moveBy(2, 3);
c.moveBy(1, 2);
r.setFillColor('red');
c.setFillColor('blue');
console.log(r);
console.log(c);
输出:
标记为 // 1
的行在矩形和圆形的情况下打印 white
。
标记为 // 2
的行打印矩形 red
和圆形 blue
。
最终输出为:
{
"type": "rectangle",
"fillColor": "white",
"dimensions": {
"x": 3,
"y": 4,
"width": 10,
"height": 10,
"fillColor": "red"
}
}
{
"type": "circle",
"fillColor": "white",
"dimensions": {
"x": 11,
"y": 12,
"diameter": 10,
"fillColor": "blue"
}
}
作为对象属性的fillColor
仍然是white
。
但是,dimensions
里面的那个已经取到了正确的值。
问题源于 createShape
中的作业 - 我的注释:
// creating the "new object"
shape = {
...shape, // shallow copying of the "old object"
...withSetFillColor(shape),
...withMoveBy(shape),
};
在这里,您创建一个 新对象,它由以下内容组成:
- 现有
...shape
的浅复制属性(类型,填充颜色,维度是一个对象)
setFillColor
,一个绑定到 shape
的闭包(旧对象)
moveBy
,一个绑定到 shape
的闭包(旧对象)
这条语句执行后,你创建了两个形状:
- 方法操作的“旧对象”
- 你return
的“新对象”
从旧对象复制的属性中,只有 维度 是非原始值,因此在实例之间共享。
然后,当您调用:
r.moveBy(2, 3);
它改变了 oldShape.dimensions
,但它与 newShape.dimensions
是同一个对象,所以它在输出中可见。
但是,这个调用:
r.setFillColor('red');
修改 oldShape
的 fillColor
属性,您看不到。它还写入 oldShape.dimensions.fillColor
,它再次在对象之间共享,因此更改在两个对象中都是可见的。
让我用 re-writing 你的代码来说明这个问题。我删除了一些细节以仅关注该问题。在代码中添加了注释和日志记录以更清楚地显示发生了什么:
const withSetFillColor = (shape) => ({
setFillColor: (color) => {
console.log(`now changing shape with id [${shape.id}]`);
shape.fillColor = color;
shape.dimensions.fillColor = color;
},
});
const shapeRectangle = (dimensions) => ({
id: 1, //add an ID of the created object for illustrative purpose
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
//variable is now named 1 to showcase what happens
let shape1 = null;
switch (type) {
case 'rectangle': {
shape1 = shapeRectangle(dimensions);
break;
}
}
//this is effectively what happens when you clone and reassign an object:
//a *second one* is created but the first one persists
let shape2 = null;
if (shape1) {
shape2 = {
...shape1,
...withSetFillColor(shape1),
id: 2, //make it a different ID for illustrative purpose
};
}
console.log(`Created shape1 and shape2 and they are the same: ${shape1 === shape2}`);
console.log(`The dimensions object is the same: ${shape1.dimensions === shape2.dimensions}`);
return shape2;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
r.setFillColor('red');
console.log(r);
您创建并操作两个不同的 object。这就是代码将 属性 分配给 object 但看起来好像没有更改的原因。
有几种方法可以解决这个问题。
只创建一个object并分配给它
如果你使用Object.assign()
你可以直接换一个object而不是两个相互竞争。因此,将 object 传递给 withX()
函数将按预期工作。
const withMoveBy = (shape) => ({
moveBy: (diffX, diffY) => {
shape.dimensions.x += diffX;
shape.dimensions.y += diffY;
},
});
const withSetFillColor = (shape) => ({
setFillColor: (color) => {
shape.fillColor = color;
shape.dimensions.fillColor = color;
},
});
const shapeRectangle = (dimensions) => ({
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const shapeCircle = (dimensions) => ({
type: 'circle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
let shape = null;
switch (type) {
case 'rectangle': {
shape = shapeRectangle(dimensions);
break;
}
case 'circle': {
shape = shapeCircle(dimensions);
break;
}
}
if (shape) {
//use Object assign to only manipulate one `shape` object
Object.assign(
shape,
withSetFillColor(shape),
withMoveBy(shape)
);
}
return shape;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
let c = createShape('circle', { x: 10, y: 10, diameter: 10 });
r.moveBy(2, 3);
c.moveBy(1, 2);
r.setFillColor('red');
c.setFillColor('blue');
console.log(r);
console.log(c);
不要使用箭头函数,改用this
或者,使用常规函数或 the shorthand method definition syntax,这样您就可以使用 this
。然后,您可以将这些方法添加到您的 object 并使用 this
来引用 object,而不必将其传入。
const withMoveBy = { //no need for a function to produce the object
moveBy(diffX, diffY) { //shorthand method syntax
this.dimensions.x += diffX;
this.dimensions.y += diffY;
},
};
const withSetFillColor = { //no need for a function to produce the object
setFillColor(color) { //shorthand method syntax
this.fillColor = color;
this.dimensions.fillColor = color;
},
};
const shapeRectangle = (dimensions) => ({
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const shapeCircle = (dimensions) => ({
type: 'circle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
let shape = null;
switch (type) {
case 'rectangle': {
shape = shapeRectangle(dimensions);
break;
}
case 'circle': {
shape = shapeCircle(dimensions);
break;
}
}
if (shape) {
shape = {
...shape,
...withSetFillColor,
...withMoveBy,
};
}
return shape;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
let c = createShape('circle', { x: 10, y: 10, diameter: 10 });
r.moveBy(2, 3);
c.moveBy(1, 2);
r.setFillColor('red');
c.setFillColor('blue');
console.log(r);
console.log(c);
一种混合方法
这更多是对正在发生的事情的解释,而不是实际的新方法。
以上两种方法都有效,但显示的是同一枚硬币的两面。将 object 组合在一起称为 mixin*。 Mixins 与 object 相似 组合,因为您可以从更简单的 object 构建更复杂的 object,但由于您是通过串联实现的,因此它也是一个单独的类别。
传统上,您会使用 Object.assign(obj, mixinA, mixinB)
将内容添加到 obj
。这使得它类似于第一种方法。但是,mixinA
和 mixinB
将是实际的 object,就像第二种方法一样。
使用 class 语法,有一个有趣的替代方法可以将 mixin 添加到 class。我在这里添加它只是为了展示它 - 完全可以不使用 classes 而是使用常规的 objects。
const withMoveBy = Base => class extends Base { //mixin
moveBy(diffX, diffY) {
this.dimensions.x += diffX;
this.dimensions.y += diffY;
}
};
const withSetFillColor = Base => class extends Base { //mixin
setFillColor(color) {
this.fillColor = color;
this.dimensions.fillColor = color;
}
};
class Shape {
constructor({type, fillColor, dimensions}) {
this.type = type;
this.fillColor = fillColor;
this.dimensions = dimensions;
}
}
const shapeRectangle = (dimensions) => ({
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const shapeCircle = (dimensions) => ({
type: 'circle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
let shapeArgs = null;
switch (type) {
case 'rectangle': {
shapeArgs = shapeRectangle(dimensions);
break;
}
case 'circle': {
shapeArgs = shapeCircle(dimensions);
break;
}
}
let shape = null;
if (shapeArgs) {
//add mixins to the Shape class
const mixedInConstructor = withMoveBy(withSetFillColor(Shape));
//create the enhanced class
shape = new mixedInConstructor(shapeArgs);
}
return shape;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
let c = createShape('circle', { x: 10, y: 10, diameter: 10 });
r.moveBy(2, 3);
c.moveBy(1, 2);
r.setFillColor('red');
c.setFillColor('blue');
console.log(r);
console.log(c);
* 是的,标题是双关语。你可以笑了。
我正在学习 JS 中的组合概念。下面是我的演示代码。
moveBy
函数将值正确分配给 x
和 y
。
然而,setFillColor
函数并没有将传递的值赋值给fillColor
。
调用 setFillColor
函数时究竟发生了什么?
const withMoveBy = (shape) => ({
moveBy: (diffX, diffY) => {
shape.dimensions.x += diffX;
shape.dimensions.y += diffY;
},
});
const withSetFillColor = (shape) => ({
setFillColor: (color) => {
console.log(shape.fillColor); // 1
shape.fillColor = color;
shape.dimensions.fillColor = color;
console.log(shape.fillColor); // 2
},
});
const shapeRectangle = (dimensions) => ({
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const shapeCircle = (dimensions) => ({
type: 'circle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
let shape = null;
switch (type) {
case 'rectangle': {
shape = shapeRectangle(dimensions);
break;
}
case 'circle': {
shape = shapeCircle(dimensions);
break;
}
}
if (shape) {
shape = {
...shape,
...withSetFillColor(shape),
...withMoveBy(shape),
};
}
return shape;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
let c = createShape('circle', { x: 10, y: 10, diameter: 10 });
r.moveBy(2, 3);
c.moveBy(1, 2);
r.setFillColor('red');
c.setFillColor('blue');
console.log(r);
console.log(c);
输出:
标记为 // 1
的行在矩形和圆形的情况下打印 white
。
标记为 // 2
的行打印矩形 red
和圆形 blue
。
最终输出为:
{
"type": "rectangle",
"fillColor": "white",
"dimensions": {
"x": 3,
"y": 4,
"width": 10,
"height": 10,
"fillColor": "red"
}
}
{
"type": "circle",
"fillColor": "white",
"dimensions": {
"x": 11,
"y": 12,
"diameter": 10,
"fillColor": "blue"
}
}
作为对象属性的fillColor
仍然是white
。
但是,dimensions
里面的那个已经取到了正确的值。
问题源于 createShape
中的作业 - 我的注释:
// creating the "new object"
shape = {
...shape, // shallow copying of the "old object"
...withSetFillColor(shape),
...withMoveBy(shape),
};
在这里,您创建一个 新对象,它由以下内容组成:
- 现有
...shape
的浅复制属性(类型,填充颜色,维度是一个对象) setFillColor
,一个绑定到shape
的闭包(旧对象)moveBy
,一个绑定到shape
的闭包(旧对象)
这条语句执行后,你创建了两个形状:
- 方法操作的“旧对象”
- 你return 的“新对象”
从旧对象复制的属性中,只有 维度 是非原始值,因此在实例之间共享。
然后,当您调用:
r.moveBy(2, 3);
它改变了 oldShape.dimensions
,但它与 newShape.dimensions
是同一个对象,所以它在输出中可见。
但是,这个调用:
r.setFillColor('red');
修改 oldShape
的 fillColor
属性,您看不到。它还写入 oldShape.dimensions.fillColor
,它再次在对象之间共享,因此更改在两个对象中都是可见的。
让我用 re-writing 你的代码来说明这个问题。我删除了一些细节以仅关注该问题。在代码中添加了注释和日志记录以更清楚地显示发生了什么:
const withSetFillColor = (shape) => ({
setFillColor: (color) => {
console.log(`now changing shape with id [${shape.id}]`);
shape.fillColor = color;
shape.dimensions.fillColor = color;
},
});
const shapeRectangle = (dimensions) => ({
id: 1, //add an ID of the created object for illustrative purpose
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
//variable is now named 1 to showcase what happens
let shape1 = null;
switch (type) {
case 'rectangle': {
shape1 = shapeRectangle(dimensions);
break;
}
}
//this is effectively what happens when you clone and reassign an object:
//a *second one* is created but the first one persists
let shape2 = null;
if (shape1) {
shape2 = {
...shape1,
...withSetFillColor(shape1),
id: 2, //make it a different ID for illustrative purpose
};
}
console.log(`Created shape1 and shape2 and they are the same: ${shape1 === shape2}`);
console.log(`The dimensions object is the same: ${shape1.dimensions === shape2.dimensions}`);
return shape2;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
r.setFillColor('red');
console.log(r);
您创建并操作两个不同的 object。这就是代码将 属性 分配给 object 但看起来好像没有更改的原因。
有几种方法可以解决这个问题。
只创建一个object并分配给它
如果你使用Object.assign()
你可以直接换一个object而不是两个相互竞争。因此,将 object 传递给 withX()
函数将按预期工作。
const withMoveBy = (shape) => ({
moveBy: (diffX, diffY) => {
shape.dimensions.x += diffX;
shape.dimensions.y += diffY;
},
});
const withSetFillColor = (shape) => ({
setFillColor: (color) => {
shape.fillColor = color;
shape.dimensions.fillColor = color;
},
});
const shapeRectangle = (dimensions) => ({
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const shapeCircle = (dimensions) => ({
type: 'circle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
let shape = null;
switch (type) {
case 'rectangle': {
shape = shapeRectangle(dimensions);
break;
}
case 'circle': {
shape = shapeCircle(dimensions);
break;
}
}
if (shape) {
//use Object assign to only manipulate one `shape` object
Object.assign(
shape,
withSetFillColor(shape),
withMoveBy(shape)
);
}
return shape;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
let c = createShape('circle', { x: 10, y: 10, diameter: 10 });
r.moveBy(2, 3);
c.moveBy(1, 2);
r.setFillColor('red');
c.setFillColor('blue');
console.log(r);
console.log(c);
不要使用箭头函数,改用this
或者,使用常规函数或 the shorthand method definition syntax,这样您就可以使用 this
。然后,您可以将这些方法添加到您的 object 并使用 this
来引用 object,而不必将其传入。
const withMoveBy = { //no need for a function to produce the object
moveBy(diffX, diffY) { //shorthand method syntax
this.dimensions.x += diffX;
this.dimensions.y += diffY;
},
};
const withSetFillColor = { //no need for a function to produce the object
setFillColor(color) { //shorthand method syntax
this.fillColor = color;
this.dimensions.fillColor = color;
},
};
const shapeRectangle = (dimensions) => ({
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const shapeCircle = (dimensions) => ({
type: 'circle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
let shape = null;
switch (type) {
case 'rectangle': {
shape = shapeRectangle(dimensions);
break;
}
case 'circle': {
shape = shapeCircle(dimensions);
break;
}
}
if (shape) {
shape = {
...shape,
...withSetFillColor,
...withMoveBy,
};
}
return shape;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
let c = createShape('circle', { x: 10, y: 10, diameter: 10 });
r.moveBy(2, 3);
c.moveBy(1, 2);
r.setFillColor('red');
c.setFillColor('blue');
console.log(r);
console.log(c);
一种混合方法
这更多是对正在发生的事情的解释,而不是实际的新方法。
以上两种方法都有效,但显示的是同一枚硬币的两面。将 object 组合在一起称为 mixin*。 Mixins 与 object 相似 组合,因为您可以从更简单的 object 构建更复杂的 object,但由于您是通过串联实现的,因此它也是一个单独的类别。
传统上,您会使用 Object.assign(obj, mixinA, mixinB)
将内容添加到 obj
。这使得它类似于第一种方法。但是,mixinA
和 mixinB
将是实际的 object,就像第二种方法一样。
使用 class 语法,有一个有趣的替代方法可以将 mixin 添加到 class。我在这里添加它只是为了展示它 - 完全可以不使用 classes 而是使用常规的 objects。
const withMoveBy = Base => class extends Base { //mixin
moveBy(diffX, diffY) {
this.dimensions.x += diffX;
this.dimensions.y += diffY;
}
};
const withSetFillColor = Base => class extends Base { //mixin
setFillColor(color) {
this.fillColor = color;
this.dimensions.fillColor = color;
}
};
class Shape {
constructor({type, fillColor, dimensions}) {
this.type = type;
this.fillColor = fillColor;
this.dimensions = dimensions;
}
}
const shapeRectangle = (dimensions) => ({
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const shapeCircle = (dimensions) => ({
type: 'circle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
let shapeArgs = null;
switch (type) {
case 'rectangle': {
shapeArgs = shapeRectangle(dimensions);
break;
}
case 'circle': {
shapeArgs = shapeCircle(dimensions);
break;
}
}
let shape = null;
if (shapeArgs) {
//add mixins to the Shape class
const mixedInConstructor = withMoveBy(withSetFillColor(Shape));
//create the enhanced class
shape = new mixedInConstructor(shapeArgs);
}
return shape;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
let c = createShape('circle', { x: 10, y: 10, diameter: 10 });
r.moveBy(2, 3);
c.moveBy(1, 2);
r.setFillColor('red');
c.setFillColor('blue');
console.log(r);
console.log(c);
* 是的,标题是双关语。你可以笑了。