3

While this issue occurred to me specifically with KnockoutJS, my question is more like a general javascript question.

It is good to understand however that ko.observable() and ko.observableArray() return a method so when assigning a value to them, you need to call the target as method instead of simply assigning a value to them. The code that I'm working with should also support plain objects and arrays, which I why I need to resolve to a method to call to assign a value to the target.

Think of these 2 examples:

Non-working one (this context changed in called method):

// Assigning value to the target object
var target;

// target can be of any of thr following types
target = ko.observableArray(); // knockout observable array (function)
// target = ko.observable(); // knockout observable (function)
// target = {}; // generic object
// target = []; // generic array

//#region resolve method to call
var method;

if (isObservable(target)) {
    // if it is a knockout observable array, we need to call the target's push method
    // if it is a konckout observable, we need to call the target as a method
    method = target.push || target;
} else {
    // if target is a generic array, we need to use the array's push prototype
    // if target is a generic object, we need to wrap a function to assign the value
    method = target.push || function(item){ target = item; };
}
//#endregion

// call resolved method
method(entity);

Working one (this context is fine):

if (isObservable(target)) {
    if (target.push) {
        target.push(entity);
    } else {
        target(entity);
    };
} else {
    if (target.push) {
        target.push(entity);
    } else {
        target = entity;
    };
}

Now, to the actual question:

In the first approach, later in the execution chain when using a knockout observable knockout refers to this context within itself, trying to access the observable itself (namely this.t() in case someone is wondering). In this particular case due to the way of callin, this has changed to window object instead of pointing to the original observable.

In the latter case, knockout's this context is just normal.

Can any of you javascript gurus tell me how on earth my way of calling can change the 'this' context of the function being called?

Ok, I know someone wants a fiddle so here goes :)

Method 1 (Uncaught TypeError: Object [object global] has no method 'peek')

Method 2 (Works fine)

P.S. I'm not trying to fix the code, I'm trying to understand why my code changes the this context.


UPDATE:

Thanks for the quick answers! I must say I hate it when I don't know why (and especially how) something is happening. From your answers I fiddled up this quick fiddle to repro the situation and I think I got it now :)

// So having an object like Foo
function Foo() {
    this.dirThis = function () {
        console.dir(this);
    };
};

// Instantiating a new Foo
var foo = new Foo();

// Foo.dirThis() has it's original context
foo.dirThis(); // First log in console (Foo)

// The calling code is in Window context
console.dir(this); // Second log in console (Window)

// Passing a reference to the target function from another context
// changes the target function's context
var anotherFoo = foo.dirThis;

// So, when being called through anotherFoo, 
// Window object gets logged
// instead of Foo's original context
anotherFoo(); // 3rd log


// So, to elaborate, if I create a type AnotherFoo
function AnotherFoo(dirThis){
    this.dirThis = dirThis;
}

// And and instantiate it
var newFoo = new AnotherFoo(foo.dirThis);

newFoo.dirThis(); // Should dir AnotherFoo (4th in log)
Jani Hyytiäinen
  • 5,293
  • 36
  • 45
  • 1
    You're asking why? It's because that's the way the language was designed. The "context" (`this` value) is set based on the way the function is invoked, just as you described in your title. – cookie monster Dec 14 '13 at 14:14
  • 1
    This answer contains a great explanation about how `this` works in JavaScript: http://stackoverflow.com/a/134149/1443358 – Guilherme Sehn Dec 14 '13 at 14:21
  • 1
    This is extremely long winded; you should post the **shortest** example that reproduces your problem, and in this case, it would be about two lines of code. – user229044 Dec 14 '13 at 14:29
  • For a **complete** description of how `this` works in javascript see: http://stackoverflow.com/questions/13441307/how-does-the-this-keyword-in-javascript-act-within-an-object-literal/13441628#13441628 (I keep updating the answer when newer language features add new ways `this` can behave). Note that currently (as of December 2013) there are 7 ways `this` can behave. – slebetman Dec 14 '13 at 14:40
  • @slebetman: There haven't been any new ways since 2009. :-p – cookie monster Dec 14 '13 at 14:56
  • @cookiemonster: December 2013 is today's date. If 2 years from now someone reads my comment and complains that javascript has 9 behaviors for "this" I can point him to the date I made the comment – slebetman Dec 14 '13 at 15:31
  • I've been on SO long enough to have answers to questions go from DEFINITELY TRUE to EXCEPT IN IE to FALSE – slebetman Dec 14 '13 at 15:33
  • @meagar I beg to disagree. It would be 0 lines of code. Going that far with the narrowing down would have made me realize why it's working the way it's working thus ultimately I wouldn't have needed to post anything here. – Jani Hyytiäinen Jan 21 '14 at 14:22

2 Answers2

3

If you're after a way to choose the 'this' that will get used at the time of call, you should use bind, that's exactly done for that.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

So if SomeObject has a push method, then storing it like this won't work :

var thePushMethod = someObject.push;

since you loose the context of the function when writing this.

Now if you do :

 var thePushMethod = someObject.push.bind(someObject);

the context is now stored inside thePushMethod, that you just call with

  thePushMethod();

Notice that you can bind also the arguments, so for instance you might write :

var pushOneLater = someObject.push.bind(someObject, 1 );

// then, later :

pushOneLater(); // will push one into someObject
GameAlchemist
  • 18,995
  • 7
  • 36
  • 59
1

Consider this example,

function Person () {
    this.fname = "Welcome";
    this.myFunc = function() {
        return this.fname;
    }
};

var a = new Person();
console.log(a.myFunc());
var b = a.myFunc;
console.log(b());

Output

Welcome
undefined

When you make a call to a.myFunc(), the current object (this) is set as a. So, the first example works fine.

But in the second case, var b = a.myFunc; you are getting only the reference to the function and when you are calling it, you are not invoking on any specific object, so the window object is assigned. Thats why it prints undefined.

To fix this problem, you can explicitly pass the this argument with call function, like this

console.log(b.call(a));

So, for your case, you might have to do this

method.call(target, entity);

Check the fixed fiddle

thefourtheye
  • 233,700
  • 52
  • 457
  • 497