Forum: PC-Programmierung Javascript - Memberfunktion als Callback


von Borislav B. (boris_b)


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?

von Carl D. (jcw2)


Lesenswert?

this in einem function Objekt zeigt natürlich auf dieses und nicht auf 
das Objekt, aus dem heraus die Funktion erzeugt wurde.
Versuch mal mit

var cbobj = this;
 ..... function() { cbobj.memberFunc(); }

Damit sollte die richtige Referenz benutzt werden.

: Bearbeitet durch User
von Borislav B. (boris_b)


Lesenswert?

Öhm, ich steh gerade auf dem Schlauch...
Wo soll der this pointer gespeichert/verwendet werden?

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


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

von Roland P. (pram)


Lesenswert?

Hast du irgend ein Framework am Laufen?  In dojo macht man das z. B so 
https://dojotoolkit.org/reference-guide/1.7/dojo/hitch.html

Gruß Roland

von Borislav B. (boris_b)


Lesenswert?

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.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


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

von D. I. (Gast)


Lesenswert?

An dieser Stelle würde ich mal folgendes einwerfen:

CoffeeScript for the win :)

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

D. I. schrieb:
> CoffeeScript for the win :)

Ja, auf jeden Fall das bessere JS: http://coffeescript.org

:-)

von Borislav B. (boris_b)


Lesenswert?

Hui, es funktioniert :-)
Als C-Denker muss man sich da ganz schön das Hirn verrenken...

Danke für die Hilfe!

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Boris P. schrieb:
> Hui, es funktioniert :-)
> Als C-Denker muss man sich da ganz schön das Hirn verrenken...

Ja, JavaScript ist nix für schwache Nerven! ;-)

von Borislav B. (boris_b)


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

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


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.

von Borislav B. (boris_b)


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
}

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Boris P. schrieb:

> OK, ich glaube ich habe es verstanden :-)

Mal sehen:


>   this.start = function() {
>     this.triggerUpdate(this);
      ^^^ hier wird dieses this zum ersten mal benutzt, woher sollte 
triggerUpdate hier kommen?
>     setInterval(function(){ this.triggerUpdate(this); },
                              ^^^^ neue Funktion, neues this

Noch mal! ;-)

von D. I. (Gast)


Lesenswert?

Torsten R. schrieb:
> Coffeescript ändert daran leider auch nix. coffeescript ist meiner
> Meinung nach aber deutlich besser lesbar.

Eben, ein => tippt sich halt bedeutend schneller und liest sich besser.

von Borislav B. (boris_b)


Lesenswert?

So, jetzt funktioniert's wirklich :-)

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

Meinungen dazu?

von Carl D. (jcw2)


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
}

von Daniel A. (daniel-a)


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.

von Borislav B. (boris_b)


Lesenswert?

Daniel A. schrieb:
> Warum sollte es dass da nicht geben?

So wie ich das verstehe hat Typescript "echte" Instanz-Methoden. Siehe 
z.B. hier: 
http://blog.johnnyreilly.com/2014/04/typescript-instance-methods.html

von Daniel A. (daniel-a)


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

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.