JS 中的组合

Composition in JS

我正在学习 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里面的那个已经取到了正确的值。

问题源于 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');

修改 oldShapefillColor 属性,您看不到。它还写入 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。这使得它类似于第一种方法。但是,mixinAmixinB 将是实际的 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);

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