Javascript Angular 6: ERROR TypeError: "... is not a function" - 但它是

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/51763745/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 04:53:39  来源:igfitidea点击:

Angular 6: ERROR TypeError: "... is not a function" - but it is

javascriptangulartypescript

提问by Max

I am currently really confused, because I get the ERROR TypeError: "_this.device.addKeysToObj is not a function". But I implemented the function, so I have no idea what's the problem or why it is not callable. I have tried the code with Firefox and chrome, both through the same error.

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

The error is in line this.device.addKeysToObj(this.result.results[0]);

错误是一致的 this.device.addKeysToObj(this.result.results[0]);

Here is my class:

这是我的课:

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

And that is the call:

这就是电话:

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


Edit 1

编辑 1

Logging this.devicesee comment in code above:

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

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

Edit 2

编辑 2

Part of the device.service code:

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

The call in the parent component:

父组件中的调用:

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

Template:

模板:

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

回答by UncleDave

Original answer

原答案

This is a common gotcha with Typescript, you say deviceis of type Device, but it isn't. It has all of the same properties as a Devicewould, but since it isn't a Deviceit does not have the expected methods.

这是 Typescript 的一个常见问题,您说device是 type Device,但事实并非如此。它具有与 a 相同的所有属性Device,但由于它不是 aDevice它没有预期的方法。

You need to ensure that you instantiate Devicefor each entry in your Page, perhaps in the ngOnInitof the parent component:

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

I don't know the structure of Page, but if it's an array try the following.

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

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


Further explanation

进一步说明

Let's try a typescript example, as this behaviour doesn't have anything to do with Angular. We'll use localStorageto represent data coming from an external source, but this works just the same with HTTP.

让我们尝试一个打字稿示例,因为这种行为与 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);

That works just fine, awesome. A typescript interface is purely a compile-time structure and, unlike a class, it has no equivalent in JavaScript - it's just a developer hint. But this also means that if you create an interface for an external value, like SimpleValueabove, and get it wrongthen the compiler is still going to trust you know what you're talking about, it can't possibly validate this at compile time.

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

What about loading a class from an external source? How does it differ? If we take the example above and change SimpleValueinto a class without changing anything else then it will still work. But there is a difference. Unlike interfaces, classes are transpiled into their JavaScript equivalent, in other words, they exist past the point of compilation. In our above example this doesn't cause a problem, so let's try an example that does cause a problem.

从外部源加载类怎么样?它有什么不同?如果我们采用上面的示例并更改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

The loaded value had the properties we expected, but not the methods, uh oh! The problem is that methods get created when new SimpleClassis called. When we created valueToSavewe did indeed instantiate the class, but then we turned it into a JSON string and sent it off elsewhere, and JSON has no concept of methods so the information was lost. When we loaded the data in loadFromStoragewe did notcall new SimpleClass, we just trusted that the caller knew what the stored type would be.

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

How do we deal with this? Let's go back to Angular for a moment and consider a common use case: dates. JSON has no Date type, JavaScript does, so how do we retrieve a date from our server and have it work as a date? Here's a pattern I like to use.

我们如何处理?让我们暂时回到 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
        });
    }
}

Perhaps a bit verbose, but it's the most robust and flexible pattern I've used for dealing with rich frontend models coming from flat backend contracts.

也许有点冗长,但它是我用来处理来自扁平后端合同的丰富前端模型的最健壮和灵活的模式。