为什么我的变量总是在此函数内发生突变(如果函数甚至没有碰到它)?

时间:2019-06-20 13:10:07

标签: javascript angular typescript

我从api中检索了一个对象,将其保存在一个变量中,该变量可能不再被修改,但是被意外修改了

我检查了整个组件,所有功能以及html,以查找正在修改的位置,但没有成功。 我也已经更改了变量名称,以希望成为任何变量冲突,但也没有成功。 请记住,我之所以仅发布此问题,是因为我对该问题一无所知(它可能并且可能是,作为开发人员的我对javascript的一些无知),所以请客气

有问题的功能:

  addArtigo(artigoInStock: Artigo) {
    console.log('log 1:', this.assistenciaOpen.material[1].qty, this.material[1].qty); // log1: 2 2
    const artigo = { ...artigoInStock, qty: 1 };
    let materialQ = this.material;
    if (artigoInStock.qty > 0) {
      if (materialQ) {
        if (materialQ.findIndex(item => item.id === artigo.id) < 0) {
          materialQ = [...materialQ, artigo];
        } else {
          console.log('log 2:', this.assistenciaOpen.material[1].qty, this.material[1].qty); // log2: 2 2
          materialQ.map(
            itemQ => {
              if (+itemQ.id === +artigo.id) {
                itemQ.qty++;
                return itemQ;
              } else {
                return itemQ;
              }
            }
          );
          console.log('log 3:', this.assistenciaOpen.material[1].qty, this.material[1].qty); // log3: 3 3
        }
      } else {
        materialQ = [artigo];
      }
      const resultIndex = this.results.findIndex(result => result.id === artigo.id);
      this.results[resultIndex].qty = this.results[resultIndex].qty - artigo.qty;
      this.material = materialQ;
      this.modal = false;
    }
    console.log('log 4:', this.assistenciaOpen.material[1].qty, this.material[1].qty); // log4: 3 3
    // I was expecting log3 to be log3: 2 3 and log4:2 3
    // for some reason, after materialQ.map() the this.assistenciaOpen.material[1].qty change, unexpectedly, from 2 to 3 (mirroring the this.material[1].qty)
  }

}

完整组件:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { map, concatMap, tap, toArray } from 'rxjs/operators';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe';

import { Assistencia, Artigo } from 'src/app/shared/models';
import { PrintService } from 'src/app/pages/dashboard-page/prints/print.service';
import { UIService, UI } from 'src/app/shared/state/ui.service';
import { AssistenciasService, ArtigosService } from 'src/app/shared/state';
import { Observable, concat, of } from 'rxjs';
import { FormBuilder } from '@angular/forms';

@AutoUnsubscribe()
@Component({
  selector: 'app-assistencia-page',
  templateUrl: './assistencia-page.component.html',
  styleUrls: ['./assistencia-page.component.scss']
})
export class AssistenciaPageComponent implements OnInit, OnDestroy {
  public assistenciaOpen: Assistencia;
  public modal = false;
  public artigoSearchForm = this.fb.group({
    input: [null]
  });
  public results: Artigo[];
  public material: Partial<Artigo>[];

  constructor(
    private printService: PrintService,
    private uiService: UIService,
    private assistencias: AssistenciasService,
    private artigos: ArtigosService,
    private router: Router,
    private route: ActivatedRoute,
    private fb: FormBuilder) {
  }

  ngOnInit() {
    this.route.paramMap
      .pipe(
        concatMap((params: ParamMap) => this.assistencias.getAndWatch(+params.get('id'))),
        map((res: Assistencia[]) => res[0]),
        concatMap(
          assistencia => {
            let assistMaterial: Partial<Artigo>[];
            typeof assistencia.material === 'string'
              ? assistMaterial = JSON.parse(assistencia.material)
              : assistMaterial = assistencia.material;
            if (assistMaterial) {
              return concat(
                assistMaterial.map(
                  (artigo: Partial<Artigo>) => {
                    return this.artigos.get(artigo.id)
                      .pipe(
                        map((res: Artigo[]) => res[0]),
                        map(res => res = { ...res, qty: artigo.qty })
                      );
                  }
                )
              ).pipe(
                concatMap(concats => concats),
                toArray(),
                map( material => ({...assistencia, material})));
            } else {
              return of(assistencia);
            }
          }
        ),
        tap( assistencia => this.assistenciaOpen = assistencia)
      )
      .subscribe(
        (assistencia) => {
          console.log(assistencia);
          console.log('init');
          this.material = assistencia.material;
        }
      );
  }

  ngOnDestroy() { }

  saveChangesOnStock(material: Partial<Artigo>[], assistMaterial: Partial<Artigo>[]) {
    if (material) {
      console.log(material);
      return concat(material.map(
        (artigo: Artigo) => this.artigos.get(artigo.id)
          .pipe(
            map(res => res[0]),
            concatMap(
              (dbArtigo: Artigo) => {
                const artigoToSave = { id: dbArtigo.id, qty: dbArtigo.qty - (artigo.qty) };
                return this.artigos.patch(dbArtigo.id, artigoToSave);
              }
            )
          )
      )).pipe(concatMap(a => a), toArray());
    } else {
      console.log(null);
      return of(null);
    }
  }

  saveAssistencia(newEstado: string, assistencia: Assistencia) {
    if (newEstado !== 'em análise' && !assistencia.relatorio_cliente) {
      return alert('Preenche o relatório para o cliente!');
    }
    return this.saveChangesOnStock(this.material, null)
      .pipe(
        concatMap(
          _ => this.assistencias.patch(assistencia.id, { ...assistencia, estado: newEstado, material: this.material })
            .pipe(
              tap(() => {
                if (newEstado === 'entregue') { this.printService.printAssistenciaSaida(assistencia); }
              }),
              tap(() => window.history.back())
            )
        )
      ).subscribe();
  }

  openNewAssistenciaWithThisData(assistencia: Assistencia) {
    this.uiService.patchState(
      {
        // modals
        // pages
        assistenciasCriarNovaPageContactoClienteForm: {
          contacto: assistencia.cliente_user_contacto
        },
        assistenciasCriarNovaPageCriarNovaForm: {
          ...assistencia,
          problema: `(Ficha anterior: ${assistencia.id}) `,
          orcamento: null
        },
        // prints
      }
    )
      .subscribe(() => this.router.navigate(['/dashboard/assistencias-criar-nova']));
  }

  navigateBack() {
    window.history.back();
  }

  searchArtigo(input?: string) {
    if (input) {
      const inputSplited = input.split(' ');
      const inputMapped = inputSplited.map(word =>
        '{"$or": [' +
        '{ "marca": { "$like": "%' + word + '%" }},' +
        '{ "modelo": { "$like": "%' + word + '%" }},' +
        '{ "descricao": { "$like": "%' + word + '%" }}' +
        ' ]}'
      );
      const dbQuery =
        '{' +
        '"query": {' +
        '"$limit": "200",' +
        '"$and": [' +
        inputMapped +
        ']' +
        '}' +
        '}';

      this.artigos
        .findAndWatch(JSON.parse(dbQuery))
        .pipe(
          map((artigos: Artigo[]) => {
            if (this.material) {
              artigos.map(
                artigo => {
                  const id = this.material.findIndex(item => item.id === artigo.id);
                  if (id > 0) {
                    console.log(
                      'id:' + id,
                      artigo.qty,
                      this.material[id].qty,
                      this.assistenciaOpen.material[id].qty,
                      artigo.qty - (this.material[id].qty - this.assistenciaOpen.material[id].qty));
                    artigo.qty = artigo.qty - (this.material[id].qty - this.assistenciaOpen.material[id].qty);
                    return artigo;
                  } else {
                    return artigo;
                  }
                }
              );
              return artigos;
            } else {
              return artigos;
            }
          })
        )
        .subscribe((res: Artigo[]) => this.results = res);
    }
  }

  addArtigo(artigoInStock: Artigo) {
    console.log(this.assistenciaOpen.material[1].qty, this.material[1].qty);
    const artigo = { ...artigoInStock, qty: 1 };
    let materialQ = this.material;
    if (artigoInStock.qty > 0) {
      if (materialQ) {
        if (materialQ.findIndex(item => item.id === artigo.id) < 0) {
          materialQ = [...materialQ, artigo];
        } else {
          materialQ.map(
            itemQ => {
              if (+itemQ.id === +artigo.id) {
                itemQ.qty++;
                return itemQ;
              } else {
                return itemQ;
              }
            }
          );
        }
      } else {
        materialQ = [artigo];
      }
      const resultIndex = this.results.findIndex(result => result.id === artigo.id);
      this.results[resultIndex].qty = this.results[resultIndex].qty - artigo.qty;
      this.material = materialQ;
      this.modal = false;
    }
    console.log(this.assistenciaOpen.material[1].qty, this.material[1].qty);
  }

}

HTML

<div *ngIf="!assistenciaOpen">
  <span class="spinner spinner-inline">
  </span>
  <span>
    A carregar dados...
  </span>
</div>

<div *ngIf="assistenciaOpen as assistencia">
  <button type="button" class="btn btn-link btn-icon" style="margin:0; padding:0" (click)="navigateBack()">
    <clr-icon class="is-solid" size="36" shape="arrow" style="transform: rotate(270deg);"></clr-icon>
  </button>

  <h3>Assistência {{assistencia.id}}</h3>

  <div class="separate">
    <div class="clr-row">
      <div class="clr-col-md-2 fontheavy">
        Estado:
      </div>
      <div class="clr-col">
        {{assistencia.estado}}
      </div>
    </div>
  </div>
  <div class="separate">
    <div class="clr-row">
      <div class="clr-col-md-2 fontheavy">
        Cliente:
      </div>
      <div class="clr-col">
        {{assistencia.cliente_user_name}}
      </div>
    </div>
    <div class="clr-row">
      <div class="clr-col-md-2 fontheavy">
        Técnico:
      </div>
      <div class="clr-col">
        {{assistencia.registo_cronologico[assistencia.registo_cronologico.length-1].tecnico}}
      </div>
    </div>
    <div class="clr-row">
      <div class="clr-col-md-2 fontheavy">
        Equipamento:
      </div>
      <div class="clr-col">
        {{assistencia.categoria}}
        {{assistencia.marca}}
        {{assistencia.modelo}}
        {{assistencia.cor}}
      </div>
    </div>
    <div class="clr-row">
      <div class="clr-col-md-2 fontheavy">
        Serial:
      </div>
      <div class="clr-col">
        {{assistencia.serial}}
      </div>
    </div>
    <div class="clr-row">
      <div class="clr-col-md-2 fontheavy">
        Problema:
      </div>
      <div class="clr-col">
        {{assistencia.problema}}
      </div>
    </div>
  </div>
  <div class="separate">
    <div class="clr-row">
      <div class="clr-col">
        <div class="clr-row">
          <div class="clr-col">
            <button class="btn btn-link" (click)="modal = true">Adicionar material</button>
          </div>
        </div>
        <div class="clr-row" *ngIf="material">
          <div class="clr-col">
            <table class="table">
              <thead>
                <tr>
                  <th class="left">Qty</th>
                  <th>Descrição</th>
                  <th>Local</th>
                  <th class="left">Custo p/ unidade</th>
                </tr>
              </thead>
              <tbody>
                <tr *ngFor="let artigoY of material">
                  <td class="left">{{artigoY.qty}}</td>
                  <td>{{artigoY.descricao}} {{artigoY.marca}} {{artigoY.modelo}}</td>
                  <td>{{artigoY.localizacao}}</td>
                  <td class="left">{{artigoY.preco | currency: 'EUR'}}</td>
                </tr>
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  </div>
  <div class="separate">
    <div class="clr-row">
      <div class="clr-col-md-6">
        <div class="clr-row">
          <div class="clr-col fontheavy">
            Informação técnica interna:
          </div>
        </div>
        <div class="clr-row">
          <div class="clr-col">
            <!--
            <textarea name="relatorio_interno" id="relatorio_interno" [attr.readonly]="assistencia.estado === 'contacto pendente'
                || assistencia.estado === 'não atendeu p/ cont.'
                || assistencia.estado === 'cliente adiou resp.'
                || assistencia.estado === 'orçamento pendente'
                || assistencia.estado === 'não atendeu p/ orç.'
                || assistencia.estado === 'cliente adiou orç.'
                || assistencia.estado === 'aguarda material'
                || assistencia.estado === 'concluído'
                || assistencia.estado === 'entregue'" [(ngModel)]='assistencia.relatorio_interno'></textarea>
              -->
            <textarea name="relatorio_interno" id="relatorio_interno"
              [(ngModel)]='assistencia.relatorio_interno'></textarea>
          </div>
        </div>
      </div>
      <div class="clr-col-md-6">
        <div class="clr-row">
          <div class="clr-col fontheavy">
            Informação para o cliente:
          </div>
        </div>
        <div class="clr-row">
          <div class="clr-col">
            <textarea name="relatorio_cliente" id="relatorio_cliente"
              [(ngModel)]='assistencia.relatorio_cliente'></textarea>
          </div>
        </div>
      </div>
    </div>
  </div>
  <div class="separate">
    <div class="clr-row">
      <div class="clr-col-md-2 fontheavy">
        Orçamento:
      </div>
      <div class="clr-col">
        <span *ngIf="assistencia.orcamento">€ {{assistencia.orcamento}}</span>
        <span *ngIf="!assistencia.orcamento">não tem</span>
      </div>
    </div>
    <div class="clr-row">
      <div class="clr-col-md-2 fontheavy">
        Preço:
      </div>
      <div class="clr-col-md-4">
        €<input type="number" name="preco" id="preco" [(ngModel)]='assistencia.preco'>
      </div>
    </div>
  </div>

  <div class="separate">

    <div *ngIf="assistencia.estado === 'recebido'
          || assistencia.estado === 'em análise'
          || assistencia.estado === 'contactado'
          || assistencia.estado === 'incontactável'
          || assistencia.estado === 'orçamento aprovado'
          || assistencia.estado === 'orçamento recusado'
          || assistencia.estado === 'material recebido'">
      <button class="btn btn-primary btn-icon" (click)="saveAssistencia('em análise', assistencia)">
        <clr-icon class="is-solid" shape="floppy" flip="vertical"></clr-icon>
        Guardar
      </button>
      <clr-dropdown>
        <button clrDropdownTrigger class="btn btn-outline-primary">+ Opções <clr-icon shape="caret down"></clr-icon>
        </button>
        <clr-dropdown-menu clrPosition="top-left" *clrIfOpen>
          <button clrDropdownItem (click)="saveAssistencia('orçamento pendente', assistencia)">Orçamentar
          </button>
          <button clrDropdownItem (click)="saveAssistencia('contacto pendente', assistencia)">Contactar
          </button>
          <button clrDropdownItem (click)="saveAssistencia('concluído', assistencia)">Concluir</button>
        </clr-dropdown-menu>
      </clr-dropdown>
    </div>

    <div *ngIf="assistencia.estado === 'orçamento pendente'
          || assistencia.estado === 'não atendeu p/ orç.'
          || assistencia.estado === 'cliente adiou orç.'">
      <button class="btn btn-success" (click)="saveAssistencia('orçamento aprovado', assistencia)">Aceite</button>
      <clr-dropdown>
        <button clrDropdownTrigger class="btn btn-outline-primary">+ Opções <clr-icon shape="caret down"></clr-icon>
        </button>
        <clr-dropdown-menu clrPosition="top-left" *clrIfOpen>
          <button clrDropdownItem (click)="saveAssistencia('orçamento recusado', assistencia)">Recusado
          </button>
          <button clrDropdownItem (click)="saveAssistencia('cliente adiou orç.', assistencia)">Cliente adiou
            resposta</button>
          <button clrDropdownItem (click)="saveAssistencia('não atendeu p/ orç.', assistencia)">Não atendeu
          </button>
          <button clrDropdownItem (click)="saveAssistencia('incontactável', assistencia)">Incontactável
          </button>
        </clr-dropdown-menu>
      </clr-dropdown>
    </div>

    <div *ngIf="assistencia.estado === 'contacto pendente'
          || assistencia.estado === 'não atendeu p/ cont.'
          || assistencia.estado === 'cliente adiou resp.'">
      <button type="button" class="btn btn-success" (click)="saveAssistencia('contactado', assistencia)">
        Sucesso
        <clr-icon shape="caret down"></clr-icon>
      </button>
      <clr-dropdown>
        <button clrDropdownTrigger class="btn btn-outline-primary">+ Opções <clr-icon shape="caret down"></clr-icon>
        </button>
        <clr-dropdown-menu clrPosition="top-left" *clrIfOpen>
          <button type="button" (click)="saveAssistencia('contactado', assistencia)" clrDropdownItem>Cliente adiou
            resposta</button>
          <button type="button" (click)="saveAssistencia('não atendeu p/ cont.', assistencia)" clrDropdownItem>Não
            atendeu</button>
          <button type="button" (click)="saveAssistencia('incontactável', assistencia)"
            clrDropdownItem>Incontactável</button>
        </clr-dropdown-menu>
      </clr-dropdown>
    </div>

    <div *ngIf="assistencia.estado === 'concluído'">
      <button class="btn btn-warning" (click)="saveAssistencia('entregue', assistencia)">
        Entregar
      </button>
    </div>

    <div *ngIf="assistencia.estado === 'entregue'">
      <button class="btn" (click)="openNewAssistenciaWithThisData(assistencia)">
        Receber Novamente
      </button>
    </div>
  </div>

</div>

<clr-modal [(clrModalOpen)]="modal" [clrModalSize]="'lg'">
  <h3 class="modal-title">Adicionar Material (? Em construção...)</h3>
  <div class="modal-body">
    <form [formGroup]="artigoSearchForm" (ngSubmit)="searchArtigo(artigoSearchForm.value.input)">
      <div class="clr-row">
        <div class="clr-col-12 padding-bottom-05">
          <input type="text" class="wd-100" placeholder="Digita algo..." name="input" formControlName="input" />
        </div>
      </div>
    </form>
    <h4>Resultados:</h4>
    <div *ngIf="results as artigos">
      <div *ngIf="artigos.length===0">? Não encontrei nada... Tenta novamente com atenção.</div>
      <div *ngIf="artigos.length>0">
        <r-data-row class="clr-row" *ngFor="let artigo of artigos; let listaIndex=index" (click)="addArtigo(artigo)">
          <div class="clr-col-lg-3">
            <span class="fontheavy">{{artigo.marca}}</span>
            {{artigo.modelo}}
          </div>
          <div class="clr-col-lg-6 lessimportant">
            {{artigo.descricao}}
          </div>
          <div class="clr-col-lg-1 lessimportant">
            Qty: {{artigo.qty}}
          </div>
          <div class="clr-col-lg-2 lessimportant">
            Local: {{artigo.localizacao}}
          </div>
        </r-data-row>
      </div>
    </div>
  </div>
</clr-modal>

我希望this.assistenciaOpen仅被修改1次(当我通过ngOnInit()方法订阅api时

2 个答案:

答案 0 :(得分:0)

我能想到的唯一原因是

  

点击(Assistencia => this.assistenciaOpen =助手)

您可以将其更改为

  

点击(Assistencia => {console.log(“获得一个值”); this.assistenciaOpen   = Assistencia})

如果您看到这两个控制台转储之间有一个一个值,那么这就是原因。请在此处添加逻辑以处理此重新分配。

答案 1 :(得分:0)

好吧,我发现我的代码出了什么问题,我正在发布解决方案,以便以后可以为他人提供帮助。
我怀疑问题是由我对javascript如何复制对象的无知造成的。

说明

尽管上面的函数有一些错误
(如:
->错误使用.map()方法
->在.map()内使用增量运算器
->加上由于其不纯正的行为,我根本不建议在任何地方使用++increment--decrement算术运算符)

我完全不知道javascript是如何复制对象的。

在javascript中,当您从原始类型(Number String Boolean undefinednull)复制值时,您正在创建值的副本转到新变量。
当您复制objects(如objectarray等)时,您实际上只是在新变量中创建了对旧变量的引用。

因此,当您更改变量的副本时,实际上是在更改原始变量。

问题示例

const a = 4;
const b = a; // b = 4
++b;
console.log(a, b); // 4 5
const car = {
    motor: {
        fuel: 'gas',
        intake: 'compressor'
    },
    color: red
};
const otherCar = car;
otherCar.motor.intake = 'turbo';

console.log(car.motor.intake); // turbo
console.log(otherCar.motor.intake); // turbo

解决方案

为解决此问题,我找到了很多解决方案,从中我将指出2:
1-将库用作 immutablejs 来确保不变性(如果您使用typescript,则将很难实现,如果您想将immutable.Map对象与类型一起使用,这是我们实现这一目标的主要原因全部使用打字稿)
2-需要复制时,使用... 传播算子复制 1级对象,或使用Ramda.clone复制深克隆 嵌套对象

警告

您可以将库用作 Ramdajs 来制作嵌套对象的深层副本,但请明智地使用它,因为深层副本会对性能产生影响 >。
就我而言,我正在构建一个中等大小的webApp,但我什么也没注意到,但是了解这一点很重要。

奖金

您可以在freecodecamp.com上阅读this article,以了解有关javascript如何处理复制的更多信息