Marecky Marecky - 2 months ago 10
Javascript Question

how to call 'this' outside my objects scope?

I have developed some sort of Jcrop initialization for a website, I managed to make my own namespace. Question I have is regarding this keyword. Every time I had to access my base object "aps" in any callback function I must wrap this in a variable (I have chosen word that). Is there any better way to do it? For example can I use call or apply methods? This is just a namespace so I could use simple aps.methodName but for sake of this example, please don't mind it. Here is my source code:

var aps;

$(function(){
aps = function(){

// private
// variables

var bgColor = '#f5f5f5';
var threshold = 370;
var threshold_width = 800;

return {
tmpl : $('#jcrop-template').html(),
upl_cont : {},
form : {},
logo_img : new Image(),
jcrop_api : null,
scaled_logo_url : '',
image_filename : '',
original_image_filename : '',
mime : '',
trueSize : '',

jcrop_init : function (oiFrameRes){
$('#logo_upload_form').find('img').hide();
this.scaled_logo_url = oiFrameRes.image_url;
this.logo_url = oiFrameRes.original_image_url;
this.original_image_filename = oiFrameRes.original_image_filename;
this.image_filename = oiFrameRes.image_filename;
this.mime = oiFrameRes.mime;
this.upl_cont = $('#facebox div#upload-container-d');
this.logo_img = new Image();
this.logo_img.that = this;
this.logo_img.name = 'logo';
this.logo_img.onload = function(){
this.true_width=this.width;
this.true_height=this.height;
this.that.resize_image();
this.that.resize_facebox();
this.that.display_image();
}
this.logo_img.src = this.logo_url;
},

resize_image : function(){
this.trueSize = '';
if(typeof (this.oSettings.trueSize)!=='undefined') delete(this.oSettings.trueSize);
if (this.logo_img.width > threshold){
if (this.logo_img.width > threshold_width){
this.trueSize = [ this.logo_img.width, this.logo_img.height ];
this.logo_img.height = this.logo_img.height / (this.logo_img.width / threshold_width);
this.logo_img.width = threshold_width;
}
}
},

resize_facebox : function(){
var width = (this.logo_img.width > threshold) ? this.logo_img.width : threshold ;
$('#facebox').css({
left : $(window).width() / 2 - width / 2
}).
find('div.change-size').css({'width': width+30});
},

display_image : function (){
if (this.jcrop_api === null) {
$logo_img = $(this.logo_img).css({'display':'block','margin-left':'auto','margin-right':'auto'})
if (this.upl_cont.find('#logo-container-d>img').length > 0){
if (this.upl_cont.find('#logo-container-d>img').attr('src').length > 0){
this.upl_cont.find('#logo-container-d').empty().append($logo_img);
}
}
else {
this.upl_cont.append(this.tmpl).find('#logo-container-d').append($logo_img);
}

var that = this;
if (typeof (this.upl_cont.find('#jcrop-menu1 a').data('events')) === 'undefined'){
this.upl_cont.find('#jcrop-menu1 a').click(function(){
if (this.href.indexOf('#crop')>-1){
$(this).closest('div').hide();
that.upl_cont.find('#jcrop-menu2').show();
that.setup_crop();
}
if (this.href.indexOf('#close')>-1){
manageIframeResponse();
}
location.hash = '';
return false;
});
}
}
else {
this.reset();
}
},

reset : function(){
$('#jcrop-menu2',this.upl_cont).find('a').unbind('click').end().hide();
$('#jcrop-coords-f',this.upl_cont).find('input[type="text"]').each(function(){this.value="";}).end().hide();
$('#jcrop-menu1',this.upl_cont).find('a').unbind('click').end().show();
this.jcrop_api.destroy();
this.jcrop_api=null;
this.display_image();
},

send_form : function (){
var sPost = $(this.form).find('input[name="image_filename"]').val(this.image_filename).end()
.find('input[name="original_image_filename"]').val(this.original_image_filename).end()
.find('input[name="mime"]').val(this.mime).end()
.find('input[name="user_url"]').val($('#logo_upload_base_url').val()).end()
.find('input[name="user_key"]').val($('#logo_upload_user_key').val()).end()
.serialize();

$.ajax({
url:'iframe_upload.php',
type:'POST',
data: sPost,
success : function(response){
manageIframeResponse();
},
dataType : 'json'
});
},

setup_crop : function (){

var that = this;
if (this.jcrop_api === null) {
this.form = this.upl_cont.find('form#jcrop-coords-f').get(0);
this.upl_cont.find('#jcrop-menu2>a').click(function(){ that.send_form();return false; });
this.updateForm = function (){
var c = arguments[0];
that.form.x1.value=c.x;
that.form.x2.value=c.x2;
that.form.y1.value=c.y;
that.form.y2.value=c.y2;
that.form.h.value=c.h;
that.form.w.value=c.w;
}

this.oSettings.onSelect = this.updateForm;
if (typeof (this.trueSize) !== 'string' && $.isArray(this.trueSize)){
$.extend(this.oSettings,{'trueSize':this.trueSize});
}
$('#facebox #logo-container-d>img').Jcrop( this.oSettings, function(){

that.jcrop_api = this;
var _x1 = (that.logo_img.true_width*0.1).toFixed();
var _y1 = (that.logo_img.true_height*0.1).toFixed();
var _x2 = (that.logo_img.true_width*0.9).toFixed();
var _y2 = (that.logo_img.true_height*0.9).toFixed();
that.jcrop_api.setSelect([0,0,that.logo_img.true_width,that.logo_img.true_height]);
that.jcrop_api.animateTo([_x1,_y1,_x2,_y2]);
});
}
},

updateForm : function (){},

oSettings : {
onSelect:'',
onChange:'',
keySupport: false,
bgColor:bgColor,
aspectRatio:1,
minSize:[0,0]
}
}
}();

$(document).bind('afterClose.facebox', function() {
if (aps.jcrop_api !=null) {
aps.jcrop_api.destroy();
aps.jcrop_api=null;
}
});
});

Answer

Anytime a function is invoked using function invocation*, the this value is set to the global variable (or undefined in strict mode)—even if you call the function from a method. Douglas Crockford has actually described this as a flaw in the language.

Saving the this value into a variable that the function will have access to is the standard way of dealing with this.

If you really want to control what this is in your callback, you could use apply or call. Both take as the first argument what you want this to be set to. The difference is that apply expects all the function's arguments to be passed as an array, while call expects you to list them out individually.

So if, in your ajax callback, you wanted to call manageIframeResponse, pass it the ajax call's response (I know your example didn't pass the response, I'm just illustrating how you would do it), and have its this value be the same as the current object, you could do:

var self = this;
$.ajax({
    success : function(response){
        manageIframeResponse.apply(self, [response]); //<--- apply wants your arguments in array form
    }
});

Or, since your parameters aren't already in array form, you could more simply use call

var self = this;
$.ajax({
    success : function(response){
        manageIframeResponse.call(self, response); //<---call takes the arguments listed out one at a time
    }
});

* There are different ways to invoke a function.

Function invocation means you're just calling a function that happens to be in your current scope:

foo() //inside foo, this will be the global object (or undefined in strict mode)

Method invocation means that you're calling a function that's attached to an object

myObj.foo() //inside foo, this will be myObj

Here's an example of where this can trip you up if you're nor careful.

function objCreator() {
    var y = "There";

    function privateFunc() {
        alert(y); //alerts There as expected
        alert(this.someField); //undefined:  whoops - this is the global object, 
    }                          //so there's no someField 

    return {
        x: "Hi",
        someField: "blah",
        foo: function () {
            alert(this.x);
            privateFunc();
        }
    };
}