Jump to content

javascript XMLHttpRequest and variable lifetime and "this" scope..?


BdR
 Share

Recommended Posts

Hi, I'm trying to create a small javascript app which loads a JSON url and displays some html and divs based on the JSON data. It's got to be as small as possible so without using a frameworks like Phaser and jQuery.
 
The javascript scope of variables is a bit tricky, and I'm having some trouble getting it to work properly. My question is, how can create the MyGame class with an imageShapes image and then reference that imageShapes in the onreadystatechange method of XMLHttpRequest? Here is my code:
// namespacevar MyGame = MyGame || {    gamedata: [],    test123: 'This is a test value 123'};// game objectMyGame.start = function (jsonfilename) {    // initialise image    this.imageShapes = new Image();    // add gamedata and populate by loading json    this.gamedata = [];    this.test123 = 'This is a test value 123';    this.loadJSON(        jsonfilename,        this.onJSONsuccess, // <- function to call on successfully loaded JSON        this.onJSONerror    );}MyGame.start.prototype = {    // load a JSON file    loadJSON: function(path, success, error) {        console.log('TESTING loadJSON :  test123='+this.imageBackground);        var xhr = new XMLHttpRequest();        xhr.onreadystatechange = function()        {            if (xhr.readyState === XMLHttpRequest.DONE) {                if ((xhr.status === 200) || (xhr.status === 0)) { // status 0 = local file testing                    if (success)                        success(JSON.parse(xhr.responseText)); // <- how to reference back to "MyGame" ?                } else {                    if (error)                        error(xhr);                }            }        };        xhr.open("GET", path, true);        xhr.send();    },    // handle load json events    onJSONerror: function(xhr) {        console.log('MyGame.js - onJSONerror error loading json file');        console.error(xhr);    },    onJSONsuccess: function(data) {        // load data from JSON        this.gamedata = data;        console.log('TESTING -- data.imgshapes='+data.imgshapes+' test123='+MyGame.test123+' imageShapes='+this.imageShapes); // this.imageShapes is also undefined        MyGame.imageShapes.src = 'img/mybackground.png'; // <- problem is here, imageShapes is undefined..?    }};var test = new MyGame.start('js/mydatafile.json');

When I try the code above, it fails in onJSONsuccess. The imageShapes cannot reference and it is undefined, even though I defined it earlier in the MyGame class. This is the error:

 
Uncaught TypeError: Cannot set property 'src' of undefined  test123.js:58
Link to comment
Share on other sites

Your call to MyGame.imageShapes.src is failing because you never set imageShapes on the MyGame object. The only things on MyGame are "gamedata", "test123", and "start", which you'll see if you do console.log(MyGame).

 

The only place you're actually setting imageShapes is in the MyGame.start function. You're invoking that function "as a constructor" here:

var test = new MyGame.start('js/mydatafile.json');

, which is creating the imageShapes property on the test object. The reason it creates it on test is because when you invoke a function with the "new" keyword, several things happen:

1) A new object is created

2) The function is run with "this" set to be the new object (in your case, this will result in several properties such as imageShapes being set on this new object)

3) The new object is returned (in this case, it is returned and assigned to the "test" variable).

 

When you pass your onJSONsuccess function into your loadJSON function, it is being invoked like this:

 success(JSON.parse(xhr.responseText)); // <- how to reference back to "MyGame" ?

When you invoke a function like this, without using .call or .apply or .bind or without invoking it as a "method" of an object, the "this" object is set to be the window object. That's why you're seeing "this.imageShapes" be undefined.

Link to comment
Share on other sites

Thanks for the explanation and I've read some chapters of the "You Don't Know JS" that was very helpful, thanks for the link.  :)
 
Unfortunately it's still not working, here is my updated code:
// namespacevar MyGame = MyGame || {    gamedata: [],    test123: 'This is a test value 123',    imageShapes: null // define imageShapes here!};// game objectMyGame.start = function (jsonfilename) {    // initialise image    this.imageShapes = new Image();    // add gamedata and populate by loading json    this.gamedata = [];    this.test123 = 'This is a test value 123';    this.loadJSON(        jsonfilename,        this.onJSONsuccess,        this.onJSONerror    );}MyGame.start.prototype = {    // load a JSON file    loadJSON: function(path, success, error) {        debugger;        console.log('TESTING loadJSON :  test123='+this.imageBackground);        var xhr = new XMLHttpRequest();        xhr.onreadystatechange = function()        {            if (xhr.readyState === XMLHttpRequest.DONE) {                if ((xhr.status === 200) || (xhr.status === 0)) { // status 0 = local file testing                    if (success)                        success(JSON.parse(xhr.responseText));                } else {                    if (error)                        error(xhr);                }            }        };        xhr.open("GET", path, true);        xhr.send();    },    // handle load json events    onJSONerror: function(xhr) {        console.log('MyGame.js - onJSONerror error loading json file');        console.error(xhr);    },    onJSONsuccess: function(data) {        // load data from JSON        this.gamedata = data;        console.log('TESTING -- data.imgshapes='+data.imgshapes+' test123='+MyGame.test123+' imageShapes='+MyGame.imageShapes);        MyGame.imageShapes.src = 'img/mybackground.png';    }};

The 'success' and 'error' functions still need to be passed into the loadJSON-function I think. Else there is no way to reference the onJSONsuccess method because the onreadystatechange-function is invoked by xhr (=XMLHttpRequest). So then 'this' is set to XMLHttpRequest and then you can't reference the MyGame.start object. So in the above code the onJSONsuccess is called, but at that point 'this' is set to Window(?) and I again cannot reference MyGame.start or imageShapes.

 
Man, this is really confusing. :wacko:

 

Link to comment
Share on other sites

"this" is set to window because you're invoking the function directly. Here is a breakdown of how "this" works:

1. If you call a function directly, like this:

fun();

, then "this" is set to be window.

2. If you call a function as a method of an object, like this:

var obj = {   fun: function() {// do stuff}};obj.fun();

, then "this" will be obj.

3. You can also use Function.prototype.apply, Function.prototype.call, or Function.prototype.bind to force the value of "this".

 

In your case, you're doing number 1 because you're calling success directly. That's why "this" is the window object. 

 

And yeah, actually the way I outlined it earlier won't work (I missed the fact that you were invoking the function from the callback for onreadystatechange). Try doing this:

 loadJSON: function(path) {        var xhrCallback = function()        {            if (xhr.readyState === XMLHttpRequest.DONE) {                if ((xhr.status === 200) || (xhr.status === 0)) { // status 0 = local file testing                        this.onJSONsuccess(JSON.parse(xhr.responseText));                } else {                        this.onJSONerror(xhr);                }            }        };        var xhr = new XMLHttpRequest();        xhr.onreadystatechange = xhrCallback.bind(this);        xhr.open("GET", path, true);        xhr.send();    } 

Then in onJSONSuccess, get imageShapes from "this" instead of from MyGame:

onJSONsuccess: function(data) {        // load data from JSON        this.gamedata = data;        console.log('TESTING -- data.imgshapes='+data.imgshapes+' test123='+MyGame.test123+' imageShapes='+MyGame.imageShapes);        this.imageShapes.src = 'img/mybackground.png';    }

I made a little JSFiddle just to make sure that the context would get set correctly using this method. Open up your console and click "Run" in the JSfiddle and you'll see that "this" on onJSONsuccess is being set properly to your "MyGame.start" object. This is happening because I'm using Function.prototype.bind to force "this" to be the MyGame.function object.

Link to comment
Share on other sites

And yeah, actually the way I outlined it earlier won't work (I missed the fact that you were invoking the function from the callback for onreadystatechange). Try doing this:

 

Excellent!  :) Creating the separate callback function first, and then binding it to this worked. I think I'm starting to get the hang of this now, thanks

 

Btw you removed the link to the "You don't know JS" online books in your previous post. I found that link very helpful, so to anyone else reading you can check it out here:

https://github.com/getify/You-Dont-Know-JS

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...