Angular 10 服务在从模板中的 *ngFor 调用时未定义

Angular 10 service undefined when called from *ngFor in template

我有以下 SessionService.component.tslogout() 方法:

@Injectable({ providedIn: 'root' })
export class SessionService {
    constructor() {}

    // other session service stuff

    logout() {
        console.log('SessionService.logout');
        // do logout stuff
    }
}

下面MenuComponent.component.ts创建应用的菜单内容

import { SessionService } from '../session/session.service';

@Component({
  selector: 'app-menu',
  templateUrl: './menu.component.html',
  styleUrls: ['./menu.component.css']
})
export class MenuComponent implements OnInit {
    menuItems: Array<any>;

    constructor(private sessionService: SessionService) {}

    ngOnInit() {
        menuItems = [
            // logout menu item
            {
                name: 'Logout',
                action: this.signout
            }
        ];
    }

    // method called 'signout' to make it clear what method is called when
    signout() {
        console.log('MenuComponent.signout');
        this.sessionService.logout();
    }
}

最后,HTML 模板 MenuComponent.component.html 如下所示:

<mat-accordion class="example-headers-align" multi hideToggle displayMode="flat">
        <!-- App Signout -->
        <!-- THIS WORKS when (click)="signout()" is called -->
        <mat-expansion-panel class="menu-section mat-elevation-z0">
            <mat-expansion-panel-header (click)="signout()">
                <mat-panel-title fxLayout="row" fxLayoutAlign="start center">
                    <mat-icon class="menu-section-icon" svgIcon="logout"></mat-icon>
                    <span class="menu-section-title" tts="Logout"></span>
                </mat-panel-title>
            </mat-expansion-panel-header>
        </mat-expansion-panel>

        <!-- Menu Items inside ngFor loop -->
<!-- (click)="menuItem.action()" fails with the error described below, after this code block -->
        <mat-expansion-panel class="menu-section mat-elevation-z0" *ngFor="let menuItem of menuItems">
            <mat-expansion-panel-header (click)="menuItem.action()">
                <mat-panel-title fxLayout="row" fxLayoutAlign="start center">
                    <span class="menu-section-title">{{ menuItem.name }}</span>
                </mat-panel-title>
            </mat-expansion-panel-header>
</mat-accordion>

当我从模板中显式调用函数 signout() 时,我得到了预期的控制台输出,即

console.log('MenuComponent.signout');
console.log('SessionService.logout');

但是,当从数组中选择菜单项时,会话服务未定义,SessionService.logout() 函数永远不会执行。我收到错误

core.js:4442 ERROR TypeError: Cannot read property 'logout' of undefined at Object.signout [as action] (menu.component.ts:195) at MenuComponent_mat_expansion_panel_9_Template_mat_expansion_panel_header_click_1_listener (menu.component.html:38) at executeListenerWithErrorHandling (core.js:15225) at wrapListenerIn_markDirtyAndPreventDefault (core.js:15266) at HTMLElement. (platform-browser.js:582) at ZoneDelegate.invokeTask (zone-evergreen.js:402) at Object.onInvokeTask (core.js:27492) at ZoneDelegate.invokeTask (zone-evergreen.js:401) at Zone.runTask (zone-evergreen.js:174) at ZoneTask.invokeTask [as invoke] (zone-evergreen.js:483)

我认为它与所有组件的范围或初始化顺序有关,但对于我来说,我无法弄清楚我需要更改什么...

提前谢谢你:-)

由于您在菜单项上调用方法,它指向 menuItems 对象,使用 apply 指向当前 class 实例。

    <mat-expansion-panel class="menu-section mat-elevation-z0" *ngFor="let menuItem of menuItems">
        <mat-expansion-panel-header (click)="menuItem.action.apply(this)">
            <mat-panel-title fxLayout="row" fxLayoutAlign="start center">
                <span class="menu-section-title">{{ menuItem.name }}</span>
            </mat-panel-title>
        </mat-expansion-panel-header>

当您创建菜单项时,您会这样做:

ngOnInit() {
  menuItems = [{  name: 'Logout', action: this.signout }];
}

在这里您正在构造一个新对象,该对象具有一个名为 属性 的名称和一个名为 属性 的动作。操作 属性 引用了 this.signout 引用的函数,这就是为什么每当您调用菜单 menuItem.action 时,this 可能就是您要创建的新对象刚刚创建。

Functions/methods(没有箭头函数)在 javascript 中未绑定到它们所放置的项目。它们只存在于内存中,不同的 objects/variables 只是保留对它们的引用,重要的是它们的调用方式。

例如:

function test() { console.log(this.name); }
test() // will log undefined because we are calling the function without anything on the left side before the name and in the case this was the global scope the this would of been the global scope

const obj = { name: 'TEST', test }
obj.test() // will log "TEST" because we are calling the function with obj on the left side before the name so the context will be obj

const newObj = { name 'TEST 2', test: obj.test } // this is your case
newObj.test() // will log "TEST 2"

当使用 classes 和方法时,它是相同的,因为在 JS 中我们实际上没有 classes,它们只是语法糖:

class MyClass {
  myMethod() { console.log(this.name); }
}

下面就是

function MyClass() {

}

// and here we are just creating a property called myMethod on the 
// prototype that is referencing this function. That doesn't mean that 
// something else can't reference the function and execute it in its own 
// context and actually it will because after all without having this 
// context changing we were not going to be able to have prototypal inheritance.

MyClass.prototype.myMethod = function() {
  console.log(this.name);
};

当然,有些东西我们不想绑定它们,以便始终在我们想要的上下文中执行它们,因此我们可以使用像 bind, call and apply. Or arrow functions.

这样的方法

解决此问题的一种方法是:

ngOnInit() {
  const action = this.signout.bind(this);
  menuItems = [{  name: 'Logout', action }];
}

或者创建一个 signout class 属性 这是一个箭头函数(与第一个建议几乎相同)

export class MenuComponent implements OnInit {
    signout = () => {
        console.log('MenuComponent.signout');
        this.sessionService.logout();
    }
}

还有其他一些方法,但我认为这些是您的最佳选择。