Typescript 中 "this" 的意外值

Unexpected value of "this" in Typescript

我正在关注 Phaser tutorial 制作一个 Flappy Bird 克隆。但是在 create 方法内部,this 的值正在从 FlappyClone 对象更改:

到另一个包含大量 Phaser 东西的对象:

这导致对 this.createBird 的调用失败,因为 create 中的 this 值没有 createBird 方法。

为什么 this 会发生变化?由于 create 是一个 class 方法,难道 this 不应该引用 class 实例吗?

而且 Phaser 可能不是用不同的对象调用方法,因为 this 教程显示 this 在方法内部使用,并引用实例成员。

完整代码(app.ts):

class FlappyClone {
    private GAME_WIDTH: number = 800;
    private GAME_HEIGHT: number = 600;

    private game: Phaser.Game;
    private bird: Phaser.Sprite;

    private startState = { preload: this.preload, create: this.create, update: this.update };

    constructor() {
        this.game = new Phaser.Game(this.GAME_WIDTH, this.GAME_HEIGHT, Phaser.AUTO, "content", this.startState);

        this.game.state.add('start', this.startState, true);
    }

    preload() {
        this.game.load.image('bird', 'assets/bird.png');
        this.game.load.image('pipePiece', 'assets/pipe.png');

        this.game.physics.startSystem(Phaser.Physics.ARCADE);
    }

    create() {
        this.bird = this.createBird(this.game.world.centerX, this.game.world.centerY);

        let spaceBar = this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
        spaceBar.onDown.add(() => {
            FlappyClone.makeJump(this.bird, 500);
        }, this);


    }

    update() {
        if (this.bird.x < 0 || this.bird.y > this.GAME_HEIGHT) {
            this.restartGame();
        }
    }



    /*private createPipePiece(): Phaser.Spirit {
        var pipePiece = this.game.add.sprite(this.game.
    }*/

    private createBird(x, y): Phaser.Sprite {
        var bird = this.game.add.sprite(x, y, 'bird');
        this.game.physics.enable(bird);
        bird.body.gravity.y = 500;

        return bird;
    }

    private restartGame() {
        this.game.state.start('start');
    }

    private static makeJump(sprite: Phaser.Sprite, velocity: number) {
        sprite.body.velocity.y = -velocity;
    }

}

window.onload = () => {

    var game = new FlappyClone();

};

shouldn't this refer to the class instance

没有。这是JavaScript。 this 称为 调用上下文 并由 调用者 驱动。

更多

https://www.youtube.com/watch?v=tvocUcbCupA

快速修复

为所有 class 方法使用箭头函数:

https://basarat.gitbooks.io/typescript/content/docs/arrow-functions.html

问题

您有 classic Java脚本问题,称为 不正确的 this 上下文 this keyword in JavaScript 的行为不同于其他语言,如 C# 和 Java。

具体来说,这些引用导致丢失 this 上下文:

private startState = { preload: this.preload, create: this.create, update: this.update };

this 的工作原理

this关键字,在函数中,确定如下: * 如果函数是通过调用 .bind 创建的,this 值是提供给 bind 的参数 * 如果函数是通过方法调用 调用的 ,例如expr.func(args),那么this就是expr * 除此以外 * 如果代码在strict modethis就是undefined * 否则,thiswindow(在浏览器中)

让我们看看这在实践中是如何工作的:

class Foo {
    value = 10;
    doSomething() {
        // Prints 'undefined', not '10'
        console.log(this.value);
    }
}
let f = new Foo();
window.setTimeout(f.doSomething, 100);

此代码将打印 undefined(或者,在严格模式下,抛出异常)。 这是因为我们最终到达了上面决策树的最后一个分支。 调用了 doSomething 函数,该函数不是 bind 调用的结果,也不是在方法语法位置调用的。

我们看不到 setTimeout 的代码来了解它的调用是什么样的,但我们不需要。 需要注意的是,所有 doSomething 方法都指向 相同的函数对象 。 也就是说:

let f1 = new Foo();
let f2 = new Foo();
// 'true'
console.log(f1.doSomething === f2.doSomething);

我们知道setTimeout只能看到我们传递给它的函数,所以当它调用那个函数时, 它无法知道要提供哪个 this。 由于我们 引用 方法而没有 调用 它,this 上下文已经丢失。

红旗

一旦您了解 this 个问题,就很容易发现它们:

class Foo {
    value = 10;
    method1() {
        doSomething(this.method2); // DANGER, method reference without invocation
    }   
    method2() {
        console.log(this.value);
    }
}

解决方案

这里有几个选项,每个选项都有自己的权衡取舍。 最佳选择取决于从不同调用站点调用相关方法的频率。

Class 定义中的箭头函数

不使用正常的方法语法,而是使用 arrow function 来初始化每个实例成员。

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        // OK
        console.log(this.status);
    }
}
let d = new DemonstrateScopingProblems();
window.setTimeout(d.run); // OK
  • Good/bad:这会为 class 的每个实例的每个方法创建一个额外的闭包。如果这个方法通常只用在常规方法调用中,那就太过分了。但是,如果它在回调位置使用很多,class 实例捕获 this 上下文而不是每个调用站点在调用时创建一个新的闭包会更有效。
  • 好:外部调用者不可能忘记处理this上下文
  • 好:TypeScript 中的类型安全
  • 好:如果函数有参数则无需额外工作
  • 错误:派生的 classes 无法调用使用 super.
  • 以这种方式编写的基础 class 方法
  • 不好:哪些方法是 "pre-bound" 哪些方法不是的确切语义在您的 class 和它的消费者之间创建了一个额外的非类型安全契约。

参考站点的函数表达式

出于解释原因,此处显示了一些虚拟参数:

class DemonstrateScopingProblems {
    private status = "blah";

    public something() {
        console.log(this.status);
    }

    public run(x: any, y: any) {
        // OK
        console.log(this.status + ': ' + x + ',' + y);
    }
}
let d = new DemonstrateScopingProblems();
// With parameters
someCallback((n, m) => d.run(n, m));
// Without parameters
window.setTimeout(() => d.something(), 100);
  • Good/bad:与第一种方法
  • 相反memory/performance权衡
  • 好:在 TypeScript 中,这具有 100% 的类型安全性
  • 好:适用于 ECMAScript 3
  • 很好:您只需输入一次实例名称
  • 不好:您必须输入两次参数
  • 不好:不能轻易使用可变参数