Javascript - Memberfunktion als Callback

OP #4502290
Lesenswert?
• ▲
▼
Hi,
ich würde gerne eine Memberfunktion der Klasse A an eine andere Klasse 
übergeben. Wenn die dann aber das Callback aufrufen soll, gibt es eine 
Exception:

TypeError: this.dataUpdateCallback is not a function

Hier ist der Code:
1
// Class A
2
module.exports = function MyTestClass() {
3

4
    // Member variables
5
    this.service = null;
6

7
    // Starts the service
8
    this.start = function() {
9
        this.service = new MyCoolService(this.processNewData);
10
        this.service.start();
11
    }
12

13
    // Processes incoming data
14
    this.processNewData = function(data) {
15
        console.log(data);
16
    }
17
}
18

19
// Class B
20
module.exports = function MyCoolService(aDataUpdateCallback) {
21

22
    // Member variables
23
    this.dataUpdateCallback = aDataUpdateCallback;
24

25
    // Starts the service
26
    this.start = function() {
27
        // Do stuff, then call...
28
        this.dataUpdateCallback("Hello World!"); // => TypeError: this.dataUpdateCallback is not a function
29
    }
30
}

Hat jemand eine Idee, was ich da falsch mache?
(Firma: Torrox.de) #4502329
Lesenswert?
• ▲
▼
Boris P. schrieb:
>     this.start = function() {
>         // Do stuff, then call...
>         this.dataUpdateCallback("Hello World!"); // => TypeError:
> this.dataUpdateCallback is not a function

Das this ist ein anderes, als das ausserhalb der function(). Daher musst 
Du den äusseren this-Zeiger an die Funktion weiter reichen. Entweder wie 
Carl vorschlägt, als Variable im scope, oder z.B. als Parameter:
1
    // Starts the service
2
    this.start = function(that) {
3
        return function() {
4
            // Do stuff, then call...
5
            that.dataUpdateCallback("Hello World!");
6
        };
7
    }(this);

HTH
Torsten
(Firma: Torrox.de) #4502410
Lesenswert?
• ▲
▼
Boris P. schrieb:
> Danke so weit!
>
> Aber müsste ich dazu nicht wissen, dass "that" eine Funktion
> "dataUpdateCallback" hat? Das ist ja nicht unbedingt der Fall.
> Klasse A könnte ja eine beliebige Funktion als callback übergeben haben.

Äh??? Ich zitiere das Beispiel noch mal etwas großräumiger:
1
    // Member variables
2
    this.dataUpdateCallback = aDataUpdateCallback;
3

4
    // Starts the service
5
    this.start = function(that) {
6
        return function() {
7
            // Do stuff, then call...
8
            this.dataUpdateCallback("Hello World!");
9
        }
10
    }(this); // this == that

this.start wird initialisiert mit dem Funktionsaufruf 
function(that){...}(this). Diese Funktion liefert eine Funktion, die 
keine Parameter mehr nimmt, den this Zeiger aber über den benamten 
Parameter that gespeichert hat.

An der Stelle, an der function(that) aufgerufen wird, hat this doch 
einen member dataUpdateCallback (den hast Du doch selbst da eingeführt). 
Und eben dieses, äußere this wird als Funktionsargument an die Funktion, 
die die Funktion zurück liefert übergeben. Jede Funktion in JavaScript 
macht einen eigenen Scope auf (this beziehen sich dann immer auf die 
lokale Funktion) und kann auf den übergeordneten Scope zugreifen 
(closure).

Vielleicht wird es so deutlicher:
1
    // Member variables
2
    this.dataUpdateCallback = aDataUpdateCallback;
3

4
    // creates start function
5
    var start_factory = function(that) {
6
        return function() {
7
            // Do stuff, then call...
8
            this.dataUpdateCallback("Hello World!");
9
        }
10
    };
11

12
    // Starts the service
13
    this.start = start_factory(this);
OP #4502441
Lesenswert?
• ▲
▼
Kommando zurück, nix geht ^^
Der Callback wird gemacht, landet aber ganz wo anders!? Die Daten 
verschwinden jedenfalls im Nirwana, und landen nicht im Objekt, das den 
Service gestartet hat...

Hier mal der echte Code, so wie er nach euren Vorschlägen aussieht:
1
// Includes
2
var http = require('http');
3
var request = require('request');
4

5
// This class retrieves data from a web service
6
module.exports = function WebServiceAdapter(aUrl, aUpdateCycleTimeM, aDataUpdateCallback) {
7
  
8
  // Member variables
9
  this.url = aUrl;
10
  this.updateCycleTimeM = aUpdateCycleTimeM;
11
  this.dataUpdateCallback = aDataUpdateCallback;
12

13
  // Starts an immediate update, then runs cyclic updates
14
  this.start = function(that) {
15
    this.triggerUpdate(that);
16
    setInterval(function(){ this.triggerUpdate(that); }, this.updateCycleTimeM*60*1000);
17
  }
18
  
19
  // Performs the actual web request
20
  this.triggerUpdate = function(that) {
21
    return function() {
22
            try {
23
          // The callback function takes 3 parameters, an error, response status code and the html
24
          request(this.url, function(err, response, html) {
25
              if(err)
26
                console.log(err);
27
              else {
28
                //console.log(html);
29
                this.dataUpdateCallback(html);
30
              }
31
          });
32
      }
33
      catch(err) {
34
        console.log(err);
35
      }
36
        }
37
  }(this);
38
}

Aufgerufen wird er so:
1
this.start = function() {
2
    this.service = new WebServiceAdapter("some url", 5, this.processNewData);
3
    this.service.start(this);
4
  }

Vielleicht sollte ich mir doch mal Coffee Script ansehen? ;-)
(Firma: Torrox.de) #4502459
Lesenswert?
• ▲
▼
Du musst innerhalb von this.triggerUpdate() `this` durch `that` 
ersetzen. Dass this innerhalb von this.triggerUpdate() ist nicht 
dasselbe this, dass die Funktion triggerUpdate() enthält.

Und noch einmal: Jede Funktion in JavaScript hat ihren eigenen this 
pointer.

Coffeescript ändert daran leider auch nix. coffeescript ist meiner 
Meinung nach aber deutlich besser lesbar.
OP #4502479
Lesenswert?
• ▲
▼
Torsten R. schrieb:
> Du musst innerhalb von this.triggerUpdate() `this` durch `that`
> ersetzen. Dass this innerhalb von this.triggerUpdate() ist nicht
> dasselbe this, dass die Funktion triggerUpdate() enthält.
>
> Und noch einmal: Jede Funktion in JavaScript hat ihren eigenen this
> pointer.

OK, ich glaube ich habe es verstanden :-)
Ich gebe den this-Pointer rein, und verwende ihn in der Funktion unter 
dem Namen that, weil this dort eine andere Bedeutung hat, richtig?

Leider funktioniert es aber immer noch nicht (argh!?):
1
// Includes
2
var http = require('http');
3
var request = require('request');
4

5
// This class retrieves data from a web service
6
module.exports = function WebServiceAdapter(aUrl, aUpdateCycleTimeM, aDataUpdateCallback) {
7
  
8
  // Member variables
9
  this.url = aUrl;
10
  this.updateCycleTimeM = aUpdateCycleTimeM;
11
  this.dataUpdateCallback = aDataUpdateCallback;
12

13
  // Starts an immediate update, then runs cyclic updates
14
  this.start = function() {
15
    this.triggerUpdate(this);
16
    setInterval(function(){ this.triggerUpdate(this); }, this.updateCycleTimeM*60*1000);
17
  }
18
  
19
  // Performs the actual web request
20
  this.triggerUpdate = function(that) {
21
    return function() {
22
            try {
23
          // The callback function takes 3 parameters, an error, response status code and the html
24
          request(that.url, function(err, response, html) {
25
              if(err)
26
                console.log(err);
27
              else {
28
                //console.log(html);
29
                that.dataUpdateCallback(html);
30
              }
31
          });
32
      }
33
      catch(err) {
34
        console.log(err);
35
      }
36
        }
37
  }(this);
38
}
#4502537
Lesenswert?
• ▲
▼
Das ganze nennt sich "closure" 
https://developer.mozilla.org/de/docs/Web/JavaScript/Closures
wenn eine anonyme Funktion via function(){...} erzeugt wird, dann hat 
sie Zugriff auf Variablen im aktuellen Scope. Das geht bei JavaScript 
nur deshalb, weil jede "Variable" eigentlich nur eine Referenz auf eine 
solche ist, die irgendwo auf dem Heap liegt. Zu dem Funktionobject 
werden auch die Referenzen auf Variablen außerhalb der Funktion 
dazugepackt. Und selbst wenn eine "var" lokal definiert war, bleibt das 
Objekt, auf das die lokale "var" Referenz zeigt erhalten, selbst wenn 
man den Scope verlässt. Denn im Funktions-Objekt existiert ja noch eine 
Referenz. Nur bei this Funktioniert das nicht, denn this im 
Funktions-Objekt ist immer das Objekt selbst. Wenn man aber das 
"äußeren" this unter neuem Namen ablegt, dann klappt es:
1
// Class A
2
module.exports = function MyTestClass() {
3

4
    // Member variables
5
    this.service = null;
6

7
    // Starts the service
8
    this.start = function() {
9
        this.service = new MyCoolService(this.processNewData);
10
        this.service.start();
11
    }
12

13
    // Processes incoming data
14
    this.processNewData = function(data) {
15
        console.log(data);
16
    }
17
}
18

19
// Class B
20
module.exports = function MyCoolService(aDataUpdateCallback) {
21

22
    // Member variables
23
    this.dataUpdateCallback = aDataUpdateCallback;
24

25
    var me = this; // "rename" this
26

27
    // Starts the service
28
    this.start = function() {
29

30
        // Do stuff, then call using the MyCoolService object
31
        me.dataUpdateCallback("Hello World");
32
    }
33
}
#4502899
Lesenswert?
• ▲
▼
Boris P. schrieb:
> Das Arbeiten mit Klassen ist aber echt ein Krampf in javaScript :-/
> TypeScript scheint nach dem, was ich gerade lese, da um einiges
> "freundlicher" zu sein. Da gibt es scheinbar auch die this/that
> Problematik nicht...

Warum sollte es dass da nicht geben? Soweit ich weiss ist ECMAScript 5 
code auch gültiger TypeScript Code. In Funktionen und Classenmethoden 
gibt es immernoch ein this und eine Allgemeingültige Referenz auf die 
Classeninstanz scheint es nicht zu geben. Ergo bleibt die 
this-Problematik erhalten.

TypeScript hat ein par nette features, aber ich denke das ist ein 
sinkendes Schiff. Seit ECMAScript 6 gibt es in JavaScript auch Klassen, 
damit sind die Typisierung und Interfaces die einzigen gründe TypeScript 
weiterzuverwenden. Ausserdem finde ich die Art und Weise wie das 
Includieren von Modulen gelöst wurde für sehr unflexiebel und 
unpraktisch. Des weiteren kann ich mir sicher sein, das ECMAScript 6 in 
den Browsern implementiert wird, und das meine Programme dann Jahrzehnte 
lang ohne grössere Anpassungen laufen werden. Bei TypeScript könnte MS 
Jederzeit die Weiterentwichlung einstellen oder Änderungen vornehmen, 
die zu Inkompatiblitäten führen. Das wurde ja mit den SilverLight und 
VB6->VB.net gezeigt.
#4503458
Lesenswert?
• ▲
▼
Es sieht zwar sehr danach aus, aber eigentlich wird eine Membervariable 
mit einer Arrow Funktion initialisiert. Im Scope einer Arrow Function 
gibt es kein "this", deshalb wird das der darüberliegenden Funktion 
verwendet, was in dem fall der Konstruktor ist.

Zur Veranschaulichung:
1
class Greeter {
2
  greeting: string;
3
  constructor(message: string) {
4
    this.greeting = message;
5
  }
6
  greet = () => {
7
    return [
8
      message||"?", // <- Das ist eigentlich nicht erlaubt
9
      this.greeting||"?",
10
      (()=>this.greeting||"?")(),
11
      (function(){return this.greeting||"?";})()
12
    ].join(", ");
13
  }
14
}
15

16
document.writeln(new Greeter("test").greet());
Ausgabe:
1
test, test, test, ?

Wie man sieht ist der Parameter "message" vom Konstruktor in der 
Funktion greet vorhanden, this ist in der Arrow Funktion immernoch 
korrekt gesetzt, aber bei einer normalen Funktion immernochnicht. Es 
währe viel sinnvoller gewesen, wenn man eine variable "self" eingeführt 
hätte, welche in jeder Memberfunktion auf die Instanz zeigt.

Ausserdem ist dies eher eine Einschränkung als ein Feature:
  Die Methode nichtmehr im Prototype der Klasse vorhanden, wodurch 
TypeScript bei einer Vererbung für diese Methode extracode erzeugen 
muss. Dies hat zur Folge, dass dinge wie 
Greeter.prototype.great.call({message:"test"}) hier nichtmehr möglich 
sind. (Ich verwende das z.B. immer, wenn ich irgendwas Arrayartiges in 
ein Array umwandeln muss 
Array.prototype.slice.call({length:3,0:1,1:4,2:10}) )

Ausserdem ist es total überflüssig, weil man genausogut derartiges tun 
kann:
1
class Greeter {
2
  greeting: string;
3
  constructor(message: string) {
4
    this.greeting = message;
5
  }
6
  greet(){
7
    return "Hello "+this.greeting;
8
  }
9
}
10
var greeter = new Greeter("World");
11
var func = greeter.greet.bind(greeter);
12
document.writeln(func()); // Ausgabe "Hello World"

Nur als vergleich, so würde es in ECMAScript 6 aussehen:
1
 
2
class Greeter {
3
  constructor(message) {
4
    this.greeting = message;
5
  }
6
  greet(){
7
    return "Hello "+this.greeting;
8
  }
9
}
10
var greeter = new Greeter("World");
11
var func = greeter.greet.bind(greeter);
12
document.writeln(func()); // Ausgabe "Hello World"

Beinahe das selbe, und vollkommen MS Frei...

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren