【问题标题】:strange behaviour by cloning an object [duplicate]克隆对象的奇怪行为[重复]
【发布时间】:2021-09-18 18:38:47
【问题描述】:

我有一个小的示例 Angular 应用程序,我正在尝试使用 kendo UI 抽屉组件。我的问题与抽屉本身无关,但它具有一般性。

我有一个 items 对象数组,抽屉根据它填充:

export const items = [
  {
    text: 'France',
    icon: 'user',
    selected: false,
    expanded: false,
    parent: true,
    level: 0,
    id: '0',
    children: [
      {
        text: 'Paris',
        level: 1,
        id: '0.1',
      },
      {
        text: 'Lyon',
        selected: false,
        expanded: false,
        parent: true,
        level: 1,
        id: '0.2',
        children: [
          {
            text: 'Lyon 1',
            level: 2,
            id: '0.2.1',
          },
          {
            text: 'Lyon 2',
            level: 2,
            id: '0.2.2',
          },
        ],
      },
    ],
  },
  {
    text: 'Italy',
    icon: 'gear',
    expanded: false,
    selected: false,
    parent: true,
    level: 0,
    id: '1',
    children: [
      {
        text: 'Rome',
        level: 1,
        id: '1.1',
      },
      {
        text: 'Milan',
        level: 1,
        id: '1.2',
      },
    ],
  },
];

现在,在组件加载或用户第一次选择一个项目时,我使用这个函数克隆这个数组:

public resetItems(): Array<any> {
    const arr = [];
    items.forEach((item) => {
      arr.push(Object.assign({}, item));
    });
    return arr;
  }

然后我将返回的数组分配给一个临时数组,如下所示:

if (this.newItems.length === 0) {
      this.newItems = this.resetItems();
    }

用户可以选择、展开、折叠项目。通过单击一个项目,我相应地设置了 appr- 属性(选中、展开)。

在上面的示例数据中,我有一个父母法国和两个孩子(巴黎、里昂),其中里昂还有另外两个孩子(用于测试目的)。现在,如果我单击 France 并在 France 对象的 items 数组中展开抽屉,属性 selectedexpanded 仍然为 false。这正如预期的那样,因为我对newItems 数组而不是items 本身进行了所有这些修改。

但是如果我扩展里昂,那么里昂上的属性selectedexpandeditems 数组中设置为true,这不应该发生。为什么是这样?在我看来,newItemsitems 数组似乎是连接在一起的。但他们不应该。

我也编写了两个小函数,以便在 newItems 上设置 selectedexpanded 以将所有项目设置为 false,如果我将此属性设置为 false,如下所示:

this.clearSelection(this.newItems);
this.clearExpandedState(this.newItems);

那么只有在这种情况下,我才会有一个 items 数组,其中里昂没有被选中也没有展开。

下面是分析这个的所有组件代码。

export class AppComponent {
  public drawerExpanded = false;
  public item: any = {};
  public drawerItems = this.resetItems();
  private newItems: any[] = [];

  @ViewChild('drawer') drawer: DrawerComponent;

  public onSelect(e: DrawerSelectEvent): void {
    this.item = e.item;
    const text = e.item.text;

    // if item is no parent, then no action
    if (!this.item.parent) {
      return;
    }

    // if the drawer is collapsed and we click on one of the icons, we expand it
    if (!this.drawerExpanded) {
      this.drawer.toggle();
    }

    // we take a fresh start and reset the items to their original state,
    // but only if we click the first time on any item
    // in other cases 'newItems' must be not overridden
    if (this.newItems.length === 0) {
      this.newItems = this.resetItems();
    }

    // we set the 'selected' property on the clicked item to true (only parents should have the 'selected' property)
    // but before do it, we clear all selection
    // it has the effect, that the parent items have the '.k-state-selected' class too (background color set to primary)
    const index = this.newItems.findIndex((i) => i.text === text);
    this.clearSelection(this.newItems);
    this.newItems[index].selected = true;

    // we expand the clicked item - if not expanded - and add the children into the 'newItems' array
    // else we remove the unnecessary items based on the 'id' field and set 'expanded' to false
    if (!this.item.expanded) {
      this.newItems[index].expanded = true;
      this.addChildren(this.newItems, index, this.newItems[index].children);
    } else {
      this.newItems = this.removeChildren(this.item.id, this.newItems);
      this.newItems[index].expanded = false;
    }

    // refresh the items in the component
    this.drawerItems = this.newItems;

    console.log(items);
  }

  public addChildren(arr, index: number, children: Array<any>) {
    arr.splice(index + 1, 0, ...children);
  }

  public removeChildren(id: string, arr): Array<any> {
    return arr.filter((item) => !item.id.startsWith(`${id}.`));
  }

  public clearSelection(arr) {
    arr.forEach((item) => {
      if (item.selected) {
        item.selected = false;
      }
    });
  }

  public clearExpandedState(arr) {
    arr.forEach((item) => {
      if (item.expanded) {
        item.expanded = false;
      }
    });
  }

  public resetItems(): Array<any> {
    const arr = [];
    items.forEach((item) => {
      arr.push(Object.assign({}, item));
    });
    return arr;
  }

  toggle() {
    this.clearSelection(this.newItems);
    this.clearExpandedState(this.newItems);
    this.newItems = [];
    this.drawerItems = this.resetItems();
    this.drawer.toggle();
  }
}

【问题讨论】:

    标签: javascript arrays typescript


    【解决方案1】:

    问题在于,当您创建新数组时,您只是克隆对象的最顶层以创建新对象——所有嵌套项都保留引用:

    const items = [{ item: { name: 'apple' } }, { item: { name: 'bananas' } }];
    const arr = [];
    items.forEach((item) => {
      arr.push(Object.assign({}, item));
    });
    
    console.log(arr[0] === items[0]); // true 
    console.log(arr[0].item === items[0].item) // false
    
    items[0].item.name = 'hello';
    
    console.log(arr[0].item); // { name: 'hello' }

    要获得一个完全没有任何共享引用的新对象,您可以使用deep-cloning utility like lodash's cloneDeepJSON.parse(JSON.stringify(yourObj)),或者使用您自己的递归深度克隆实用程序。

    【讨论】:

    • 信不信由你,问完这个问题,我去厨房吃晚饭,一边热着饭,一边脑子里闪过,我只做简单的克隆,不做深度克隆。我当然会接受答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-13
    • 2021-12-13
    相关资源
    最近更新 更多