【问题标题】:Angular 6: ERROR TypeError: "... is not a function" - but it isAngular 6: ERROR TypeError: "... is not a function" - 但它是
【发布时间】:2019-01-16 17:25:07
【问题描述】:

我目前真的很困惑,因为我得到了ERROR TypeError: "_this.device.addKeysToObj is not a function"。但是我实现了这个函数,所以我不知道是什么问题或者为什么它不可调用。我已经尝试了 Firefox 和 chrome 的代码,都出现了同样的错误。

错误在this.device.addKeysToObj(this.result.results[0]);一行

这是我的课:

export class Device {
    id: number;
    deviceID: string;
    name: string;
    location: string;
    deviceType: string;
    subType: string;
    valueNamingMap: Object;

    addKeysToObj(deviceValues: object): void {
        for (let key of Object.keys(deviceValues).map((key) => { return key })) {
            if (!this.valueNamingMap.hasOwnProperty(key)) {
                this.valueNamingMap[key] = '';
            }
        }
        console.log(this, deviceValues);
    }
}

这就是电话:

export class BatterieSensorComponent implements OnInit {
    @Input() device: Device;
    public result: Page<Value> = new Page<Value>();

    //[..]

    ngOnInit() {
      this.valueService.list('', this.device).subscribe(
        res => {
          console.log(this.device);  // NEW edit 1
          this.result = res;
          if (this.result.count > 0) 
          {
            this.device.addKeysToObj(this.result.results[0]);
          }
        }
      )
    }
}

编辑 1

记录this.device见上面代码中的注释:

{
    deviceID: "000000001" 
    deviceType: "sensor"    ​
    id: 5    ​
    location: "-"
​    name: "Batteries"    ​
    subType: "sensor"    ​
    valueNamingMap:
      Object { v0: "vehicle battery", v1: "Living area battery" }
    <prototype>: Object { … } 
}

编辑 2

部分device.service代码:

list(url?: string, deviceType?: string, subType?: string): Observable<Page<Device>> {
  if(!url) url = `${this.url}/devices/`;
  if(deviceType) url+= '?deviceType=' + deviceType;
  if(subType) url+= '&subType=' + subType;

  return this.httpClient.get<Page<Device>>(url, { headers: this.headers })
    .pipe(
      catchError(this.handleError('LIST devices', new Page<Device>()))
    );
}

父组件中的调用:

ngOnInit() {
  this.deviceService.list('', 'sensor', ).subscribe(
    res => { 
      this.devices = res.results;
    }
  )
}

模板:

<div class="mdl-grid">
  <div class="mdl-cell mdl-cell--6-col mdl-cell--6-col-tablet" *ngFor="let device of devices">
    <app-batterie-sensor [device]="device"></app-batterie-sensor>
  </div>
</div>

【问题讨论】:

    标签: javascript angular typescript


    【解决方案1】:

    原答案

    这是 Typescript 的一个常见问题,你说 deviceDevice 类型,但它不是。它具有与Device 相同的所有属性,但由于它不是Device,它没有预期的方法。

    您需要确保为您的Page 中的每个条目实例化Device,也许在父组件的ngOnInit 中:

    我不知道Page 的结构,但如果是数组,请尝试以下操作。

    ngOnInit() {
      this.deviceService.list('', 'sensor', ).subscribe(
        res => { 
          this.devices = res.results.map(x => Object.assign(new Device(), x));
        }
      )
    }
    

    进一步说明

    让我们尝试一个打字稿示例,因为这种行为与 Angular 没有任何关系。我们将使用localStorage 表示来自外部源的数据,但这与 HTTP 相同。

    interface SimpleValue {
        a: number;
        b: string;
    }
    
    function loadFromStorage<T>(): T {
        // Get from local storage.
        // Ignore the potential null value because we know this key will exist.
        const storedValue = localStorage.getItem('MyKey') as string;
    
        // Note how there is no validation in this function.
        // I can't validate that the loaded value is actually T
        // because I don't know what T is.
        return JSON.parse(storedValue);
    }
    
    const valueToSave: SimpleValue = { a: 1, b: 'b' };
    localStorage.setItem('MyKey', JSON.stringify(valueToSave));
    
    const loadedValue = loadFromStorage<SimpleValue>();
    
    // It works!
    console.log(loadedValue);
    

    效果很好,太棒了。 typescript 接口纯粹是一种编译时结构,与类不同,它在 JavaScript 中没有等价物——它只是开发人员的提示。但这也意味着,如果您为外部值创建一个接口,例如上面的 SimpleValue,并且得到它错误,那么编译器仍然会相信您知道您在说什么,它无法在编译时验证这一点。

    如何从外部源加载一个类?它有什么不同?如果我们采用上面的示例并将SimpleValue 更改为一个类而不更改任何其他内容,那么它仍然可以工作。但是有区别。与接口不同,类被转译成它们的 JavaScript 等价物,换句话说,它们存在于编译点之后。在我们上面的示例中,这不会导致问题,所以让我们尝试一个确实会导致问题的示例。

    class SimpleClass {
        constructor(public a: number, public b: string) { }
    
        printA() {
            console.log(this.a);
        }
    }
    
    const valueToSave: SimpleClass = new SimpleClass(1, 'b');
    localStorage.setItem('MyKey', JSON.stringify(valueToSave));
    
    const loadedValue = loadFromStorage<SimpleClass>();
    
    console.log(loadedValue.a); // 1
    console.log(loadedValue.b); // 'b'
    loadedValue.printA(); // TypeError: loadedValue.printA is not a function
    

    加载的值有我们期望的属性,但没有方法,呃哦!问题是在调用new SimpleClass 时会创建方法。当我们创建valueToSave 时,我们确实实例化了这个类,但是我们把它变成了一个 JSON 字符串并发送到其他地方,而 JSON 没有方法的概念,所以信息丢失了。当我们在loadFromStorage 中加载数据时,我们没有调用new SimpleClass,我们只是相信调用者知道存储的类型是什么。

    我们如何处理这个问题?让我们暂时回到 Angular 并考虑一个常见的用例:日期。 JSON 没有 Date 类型,JavaScript 有,那么我们如何从服务器检索日期并将其作为日期工作?这是我喜欢使用的一种模式。

    interface UserContract {
        id: string;
        name: string;
        lastLogin: string; // ISO string representation of a Date.
    }
    
    class UserModel {
        id: string; // Same as above
        name: string; // Same as above
        lastLogin: Date; // Different!
    
        constructor(contract: UserContract) {
            // This is the explicit version of the constructor.
            this.id = contract.id;
            this.name = contract.name;
            this.lastLogin = new Date(contract.lastLogin);
    
            // If you want to avoid the boilerplate (and safety) of the explicit constructor
            // an alternative is to use Object.assign:
            // Object.assign(this, contract, { lastLogin: new Date(contract.lastLogin) });
        }
    
        printFriendlyLastLogin() {
            console.log(this.lastLogin.toLocaleString());
        }
    }
    
    import { HttpClient } from '@angular/common/http';
    import { Injectable, Component, OnInit } from '@angular/core';
    import { Observable } from 'rxjs';
    import { map } from 'rxjs/operators';
    
    @Injectable({
        providedIn: 'root'
    })
    class MyService {
        constructor(private httpClient: HttpClient) { }
    
        getUser(): Observable<UserModel> {
            // Contract represents the data being returned from the external data source.
            return this.httpClient.get<UserContract>('my.totally.not.real.api.com')
                .pipe(
                  map(contract => new UserModel(contract))
                );
        }
    }
    
    @Component({
        // bla bla
    })
    class MyComponent implements OnInit {
        constructor(private myService: MyService) { }
    
        ngOnInit() {
            this.myService.getUser().subscribe(x => {
                x.printFriendlyLastLogin(); // this works
                console.log(x.lastLogin.getFullYear()); // this works too
            });
        }
    }
    

    可能有点冗长,但它是我用来处理来自扁平后端合约的丰富前端模型的最强大、最灵活的模式。

    【讨论】:

    • 感谢您的解释!你能分享更多关于为什么会发生这种情况的信息吗?为什么对象不一样?
    • 除非您使用 new Device() 显式实例化它,否则它不是设备,原始问题中没有这样做。
    • 我正在寻找有关此行为的更多详细信息。 Angular HTTP tips for success 的帖子对我帮助很大。
    • 哈,来找函数问题的解决方案,马上找到了克隆对象的好方法。
    【解决方案2】:

    就我而言,我测试了两种适合我的解决方案

    将代码包装在 setTimeout

    ngOnInit() {
      setTimeOut({ // START OF SETTIMEOUT
        this.deviceService.list('', 'sensor', ).subscribe(
          res => { 
            this.devices = res.results.map(x => Object.assign(new Device(), x));
          }
        )
      }); // END OF SETTIMEOUT
    }
    

    其他解决方案是添加一个条件

    ngOnInit() {
      if(typeof this.deviceService.list === 'function'){ // START OF CONDITION
        this.deviceService.list('', 'sensor', ).subscribe(
          res => { 
            this.devices = res.results.map(x => Object.assign(new Device(), x));
          }
        )
      } // END OF CONDITION
    }
    

    【讨论】:

      【解决方案3】:

      您可能会遇到与接受的答案不同的问题:如果您使用 Angular 的服务而忘记了@Injectable,那么使用 Angular Ivy 您会遇到如下运行时异常:

      ERROR TypeError: ConfigurationServiceImpl.\u0275fac is not a function
      

      正确的解决方案是将 @Injectable 也添加到实现中,例如:

      // do not omit the @Injectable(), or you'll end up with the error message!
      @Injectable()
      export class ConfigurationServiceImpl implements ConfigurationService {
      ...
      }
      
      @Injectable({
        providedIn: "root",
        useClass: ConfigurationServiceImpl,
      })
      export abstract class ConfigurationService {
      ...
      }
      

      另见Angular 7 TypeError: service.x is not a function

      【讨论】:

        【解决方案4】:

        正如@UncleDave 已经解释的那样,您只是将具有相应名称的值映射到 Typescript 对象,而不是使用它创建预期的类对象。我知道这很令人困惑。

        Object.assign() 将解决您当前的问题,但如果您有嵌套对象则不会。然后,您还必须为每个嵌套对象执行 Object.assign(),如果您必须在代码库中的多个位置执行此操作,这可能会变得乏味。

        我建议另一种选择:class-transformer 使用它,您可以使用注释标记嵌套字段,告诉编译器如何创建嵌套对象。有了这个,您只需要使用plainToClass() 方法来映射您的顶级对象,所有底层字段也将具有正确的类型/对象。

        示例

        假设我们有两个类:

        class Parent {
            name: string;
            child: Child;
        
            public getText(): string {
                return 'parent text';
            }
        }
        
        class Child{
            name: string;
        
            public getText(): string {
                return 'child text';
            }
        }
        

        第一种情况我们已经知道是行不通的:

        let parentJson: any = {name: 'parent name', child: {name: 'child name'}};
        let parent: Parent = parentJson; // note: compiler accepts this because parentJson is any.  
                // If we tried to assign the json structure directly to 'parent' it would fail because the compiler knows that the method getText() is missing!
        
        console.log(parent.getText()); // throws the error that parent.getText() is not a function as expected
        

        第二种情况使用Object.assign()

        let parentJson: any = {name: 'parent name', child: {name: 'child name'}};
        let parent: Parent = Object.assign(parentJson); 
        
        console.log(parent.getText()); // this works
        console.log(parent.child.getText()); // throws error that parent.child.getText() is not a function!
        

        要使其正常工作,我们必须执行以下操作:

        let parentJson: any = {name: 'parent name', child: {name: 'child name'}};
        let parent: Parent = Object.assign(parentJson);
        parent.child = Object.assign(parentJson.child);
        
        console.log(parent.getText()); // this works
        console.log(parent.child.getText()); // this works
        

        第三种情况与类转换器:

        首先修改父类,以便定义子映射:

        class Parent {
            name: string;
            @Type(() => Child)
            child: Child;
        
            public getText(): string {
                return 'parent text';
            }
        }
        

        然后你可以映射到父对象:

        let parentJson: any = {name: 'parent name', child: {name: 'child name'}};
        let parent: Parent = plainToClass(Parent, parentJson);
        
        console.log(parent.getText()); // this works
        console.log(parent.child.getText()); // this works
        

        【讨论】:

          猜你喜欢
          • 2019-11-10
          • 2018-05-09
          • 2018-12-08
          • 1970-01-01
          • 2020-04-02
          • 2017-01-07
          • 2017-08-01
          • 2018-05-17
          • 1970-01-01
          相关资源
          最近更新 更多