【问题标题】:Composition in JSJS 中的组合
【发布时间】:2021-06-25 13:45:53
【问题描述】:

我正在学习 JS 中的组合概念。下面是我的演示代码。

moveBy 函数将值正确分配给 xy

但是,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 里面的那个值是正确的。

【问题讨论】:

  • 您创建shape,将其传递给withSetFillColor(),然后创建一个不同的shape 对象。为简单起见,我们称之为shape2。因此,当您在 shape2 上调用 withSetFillColor('red') 时,只有 shape1 被更改。两个对象共享dimensions

标签: javascript node.js function composition


【解决方案1】:

让我通过重写代码来说明问题。我删除了一些细节,只关注这个问题。在代码中添加了注释和日志记录,以更清楚地显示发生了什么:

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.assign(),您可以直接更改一个对象,而不是让两个相互竞争的对象。因此,将对象传递给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。然后,您可以将这些方法添加到您的对象中,并使用this 来引用该对象,而不必将其传入。

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);

混合方法

这更多是对正在发生的事情的解释,而不是实际的新方法。

以上两种方法都有效,但显示的是同一枚硬币的两面。将对象组合在一起称为mixin*。 Mixin 与对象组合相似,因为您可以从更简单的对象构建更复杂的对象,但也可以通过串联来实现它自己的单独类别。

传统上,您会使用Object.assign(obj, mixinA, mixinB)obj 添加内容。这使得它类似于第一种方法。但是,mixinAmixinB 将是实际对象,就像在第二种方法中一样。

使用类语法,有一个有趣的替代方法可以将 mixins 添加到类中。我在这里添加它只是为了展示它 - 不使用类并使用常规对象是完全可以的。

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);

* 是的,标题是双关语。你现在可以笑了。

【讨论】:

    【解决方案2】:

    问题源于createShape 中的此分配 - 我的注释:

        // creating the "new object"
        shape = {
          ...shape, // shallow copying of the "old object"
          ...withSetFillColor(shape),
          ...withMoveBy(shape),
        };
    

    在这里,您创建了一个新对象,它由以下各项组成:

    • 现有...shape 的浅拷贝属性(类型、填充颜色、尺寸,它是一个对象)
    • setFillColor,绑定到shape旧对象)的闭包
    • moveBy,绑定到shape旧对象)的闭包

    执行此语句后,您创建了两个形状:

    • 方法操作的“旧对象”
    • 您返回的“新对象”

    在从旧对象复制的属性中,只有 dimensions 是非原始值,因此在实例之间共享。

    然后,当你打电话时:

    r.moveBy(2, 3);
    

    它改变了oldShape.dimensions,但它与newShape.dimensions 是同一个对象,因此它在输出中可见。

    但是,这个调用:

    r.setFillColor('red');
    

    修改oldShapefillColor 属性,您没有看到。它还写入oldShape.dimensions.fillColor,这也是在对象之间共享的,因此两者的变化都是可见的。

    【讨论】:

    • 这很有见地。创建对象的正确方法应该是什么?
    • @thewebjackal 不要创建新对象 - 使用例如将属性分配给现有对象Object.assign,因此只有 1 个对 shape 的引用需要处理。
    • 好的。知道了。此外,在此示例中 - youtu.be/wfMtDGfHWpA?t=291 - 因为 Object.assign 的第一个参数是空对象,它永远不会获取名称和速度属性。就我而言,第一个参数应该是形状。或者在示例中,它应该是状态。我希望我是对的。
    猜你喜欢
    • 2017-06-19
    • 2018-03-20
    • 1970-01-01
    • 2015-08-20
    • 1970-01-01
    • 2013-08-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多